From 9c1617c9ee51a0f6b93242fe8fc789336957460c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 Apr 2018 21:15:16 -0400 Subject: drop support for Stardew Valley 1.2 (#453) --- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- .../Framework/Commands/Player/AddCommand.cs | 4 - .../Framework/Commands/Player/SetColorCommand.cs | 8 - .../Framework/Commands/Player/SetNameCommand.cs | 4 - .../Commands/World/DownMineLevelCommand.cs | 4 - .../Commands/World/SetMineLevelCommand.cs | 4 - .../Framework/ItemRepository.cs | 56 +- src/SMAPI/Constants.cs | 28 +- src/SMAPI/Events/EventArgsInput.cs | 6 - src/SMAPI/Events/EventArgsInventoryChanged.cs | 12 +- .../Events/EventArgsLocationObjectsChanged.cs | 18 +- src/SMAPI/Framework/ContentCore.cs | 83 +-- src/SMAPI/Framework/ContentManagerShim.cs | 8 - src/SMAPI/Framework/GameVersion.cs | 8 +- src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 116 ---- src/SMAPI/Framework/Reflection/ReflectedField.cs | 3 - src/SMAPI/Framework/Reflection/ReflectedMethod.cs | 3 - .../Framework/Reflection/ReflectedProperty.cs | 3 - src/SMAPI/Framework/SGame.cs | 711 +-------------------- src/SMAPI/IPrivateField.cs | 30 - src/SMAPI/IPrivateMethod.cs | 31 - src/SMAPI/IPrivateProperty.cs | 30 - src/SMAPI/IReflectionHelper.cs | 69 -- src/SMAPI/Metadata/CoreAssetPropagator.cs | 47 +- src/SMAPI/Metadata/InstructionMetadata.cs | 63 -- src/SMAPI/StardewModdingAPI.csproj | 3 - 26 files changed, 23 insertions(+), 1331 deletions(-) delete mode 100644 src/SMAPI/IPrivateField.cs delete mode 100644 src/SMAPI/IPrivateMethod.cs delete mode 100644 src/SMAPI/IPrivateProperty.cs (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index a177840c..78afa7da 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -76,7 +76,7 @@ false true - + $(GamePath)\Netcode.dll False true diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index a6f42b98..ff11da1e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -54,11 +54,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // apply quality if (match.Item is Object obj) -#if STARDEW_VALLEY_1_3 obj.Quality = quality; -#else - obj.quality = quality; -#endif else if (match.Item is Tool tool) tool.UpgradeLevel = quality; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index aa4fd105..f0815ef6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -36,11 +36,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player switch (target) { case "hair": -#if STARDEW_VALLEY_1_3 Game1.player.hairstyleColor.Value = color; -#else - Game1.player.hairstyleColor = color; -#endif monitor.Log("OK, your hair color is updated.", LogLevel.Info); break; @@ -50,11 +46,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player break; case "pants": -#if STARDEW_VALLEY_1_3 Game1.player.pantsColor.Value = color; -#else - Game1.player.pantsColor = color; -#endif monitor.Log("OK, your pants color is updated.", LogLevel.Info); break; } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs index 71e17f71..e8cb0584 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -39,11 +39,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player case "farm": if (!string.IsNullOrWhiteSpace(name)) { -#if STARDEW_VALLEY_1_3 Game1.player.farmName.Value = args[1]; -#else - Game1.player.farmName = args[1]; -#endif monitor.Log($"OK, your farm's name is now {Game1.player.farmName}.", LogLevel.Info); } else diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs index c83c3b07..2cec0fd3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs @@ -21,11 +21,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { int level = (Game1.currentLocation as MineShaft)?.mineLevel ?? 0; monitor.Log($"OK, warping you to mine level {level + 1}.", LogLevel.Info); -#if STARDEW_VALLEY_1_3 Game1.enterMine(level + 1); -#else - Game1.enterMine(false, level + 1, ""); -#endif } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs index 5947af1a..b4f6d5b3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -26,11 +26,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World // handle level = Math.Max(1, level); monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info); -#if STARDEW_VALLEY_1_3 Game1.enterMine(level); -#else - Game1.enterMine(true, level, ""); -#endif } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 9c0981c4..833fddde 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -96,28 +96,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework if (item.category == SObject.FruitsCategory) { // wine -#if STARDEW_VALLEY_1_3 - SObject wine = - new SObject(348, 1) + SObject wine = new SObject(348, 1) { Name = $"{item.Name} Wine", Price = item.price * 3 }; wine.preserve.Value = SObject.PreserveType.Wine; wine.preservedParentSheetIndex.Value = item.parentSheetIndex; -#else - SObject wine = new SObject(348, 1) - { - name = $"{item.Name} Wine", - price = item.price * 3, - preserve = SObject.PreserveType.Wine, - preservedParentSheetIndex = item.parentSheetIndex - }; -#endif yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, wine); // jelly -#if STARDEW_VALLEY_1_3 SObject jelly = new SObject(344, 1) { Name = $"{item.Name} Jelly", @@ -125,15 +113,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; jelly.preserve.Value = SObject.PreserveType.Jelly; jelly.preservedParentSheetIndex.Value = item.parentSheetIndex; -#else - SObject jelly = new SObject(344, 1) - { - name = $"{item.Name} Jelly", - price = 50 + item.Price * 2, - preserve = SObject.PreserveType.Jelly, - preservedParentSheetIndex = item.parentSheetIndex - }; -#endif yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, jelly); } @@ -141,7 +120,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework else if (item.category == SObject.VegetableCategory) { // juice -#if STARDEW_VALLEY_1_3 SObject juice = new SObject(350, 1) { Name = $"{item.Name} Juice", @@ -149,19 +127,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; juice.preserve.Value = SObject.PreserveType.Juice; juice.preservedParentSheetIndex.Value = item.parentSheetIndex; -#else - SObject juice = new SObject(350, 1) - { - name = $"{item.Name} Juice", - price = (int)(item.price * 2.25d), - preserve = SObject.PreserveType.Juice, - preservedParentSheetIndex = item.parentSheetIndex - }; -#endif yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, juice); // pickled -#if STARDEW_VALLEY_1_3 SObject pickled = new SObject(342, 1) { Name = $"Pickled {item.Name}", @@ -169,15 +137,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; pickled.preserve.Value = SObject.PreserveType.Pickle; pickled.preservedParentSheetIndex.Value = item.parentSheetIndex; -#else - SObject pickled = new SObject(342, 1) - { - name = $"Pickled {item.Name}", - price = 50 + item.Price * 2, - preserve = SObject.PreserveType.Pickle, - preservedParentSheetIndex = item.parentSheetIndex - }; -#endif yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, pickled); } @@ -211,7 +170,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // yield honey if (type != null) { -#if STARDEW_VALLEY_1_3 SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) { Name = "Wild Honey" @@ -223,18 +181,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework honey.Name = $"{item.Name} Honey"; honey.Price += item.Price * 2; } -#else - SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) - { - name = "Wild Honey", - honeyType = type - }; - if (type != SObject.HoneyType.Wild) - { - honey.name = $"{item.Name} Honey"; - honey.price += item.price * 2; - } -#endif yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, honey); } } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 1e35c030..e547dfa6 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -37,28 +37,13 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = -#if STARDEW_VALLEY_1_3 - new SemanticVersion($"2.6-alpha.{DateTime.UtcNow:yyyyMMddHHmm}"); -#else - new SemanticVersion("2.5.5"); -#endif + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion($"2.6-alpha.{DateTime.UtcNow:yyyyMMddHHmm}"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = -#if STARDEW_VALLEY_1_3 - new GameVersion("1.3.0.4"); -#else - new SemanticVersion("1.2.30"); -#endif + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.0.27"); /// The maximum supported version of Stardew Valley. - public static ISemanticVersion MaximumGameVersion { get; } = -#if STARDEW_VALLEY_1_3 - null; -#else - new SemanticVersion("1.2.33"); -#endif + public static ISemanticVersion MaximumGameVersion { get; } = null; /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -184,12 +169,7 @@ namespace StardewModdingAPI /// Get the name of a save directory for the current player. private static string GetSaveFolderName() { - string prefix = -#if STARDEW_VALLEY_1_3 - new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray()); -#else - new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray()); -#endif + string prefix = new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray()); return $"{prefix}_{Game1.uniqueIDForThisGame}"; } diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 75b9b8cd..b2bd978b 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -18,12 +18,6 @@ namespace StardewModdingAPI.Events /// The current cursor position. public ICursorPosition Cursor { get; } -#if !STARDEW_VALLEY_1_3 - /// Whether the input is considered a 'click' by the game for enabling action. - [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 - public bool IsClick => this.IsActionButton; -#endif - /// Whether the input should trigger actions on the affected tile. public bool IsActionButton { get; } diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs index b85ae9db..1fdca834 100644 --- a/src/SMAPI/Events/EventArgsInventoryChanged.cs +++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs @@ -12,11 +12,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The player's inventory. -#if STARDEW_VALLEY_1_3 public IList Inventory { get; } -#else - public List Inventory { get; } -#endif /// The added items. public List Added { get; } @@ -34,13 +30,7 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The player's inventory. /// The inventory changes. - public EventArgsInventoryChanged( -#if STARDEW_VALLEY_1_3 - IList inventory, -#else - List inventory, -#endif - List changedItems) + public EventArgsInventoryChanged(IList inventory, List changedItems) { this.Inventory = inventory; this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index 180e9d78..de6bd365 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -1,11 +1,7 @@ using System; -using Microsoft.Xna.Framework; -#if STARDEW_VALLEY_1_3 using System.Collections.Generic; +using Microsoft.Xna.Framework; using Netcode; -#else -using StardewValley; -#endif using Object = StardewValley.Object; namespace StardewModdingAPI.Events @@ -17,11 +13,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The current list of objects in the current location. -#if STARDEW_VALLEY_1_3 public IDictionary> NewObjects { get; } -#else - public SerializableDictionary NewObjects { get; } -#endif /********* @@ -29,13 +21,7 @@ namespace StardewModdingAPI.Events *********/ /// Construct an instance. /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged( -#if STARDEW_VALLEY_1_3 - IDictionary> newObjects -#else - SerializableDictionary newObjects -#endif - ) + public EventArgsLocationObjectsChanged(IDictionary> newObjects) { this.NewObjects = newObjects; } diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index 43357553..9da18481 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -44,10 +44,8 @@ namespace StardewModdingAPI.Framework /// The underlying asset cache. private readonly ContentCache Cache; -#if STARDEW_VALLEY_1_3 /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. private readonly IDictionary IsLocalisableLookup; -#endif /// The locale codes used in asset keys indexed by enum value. private readonly IDictionary Locales; @@ -111,9 +109,7 @@ namespace StardewModdingAPI.Framework this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection); this.Locales = this.GetKeyLocales(reflection); this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); -#if STARDEW_VALLEY_1_3 this.IsLocalisableLookup = reflection.GetField>(this.Content, "_localizedAsset").GetValue(); -#endif } /// Get a new content manager which defers loading to the content core. @@ -209,11 +205,7 @@ namespace StardewModdingAPI.Framework /// The language code for which to load content. /// The is empty or contains invalid characters. /// The content asset couldn't be loaded (e.g. because it doesn't exist). - public T Load(string assetName, ContentManager instance -#if STARDEW_VALLEY_1_3 - , LocalizedContentManager.LanguageCode language -#endif - ) + public T Load(string assetName, ContentManager instance, LocalizedContentManager.LanguageCode language) { // normalise asset key this.AssertValidAssetKeyFormat(assetName); @@ -221,11 +213,7 @@ namespace StardewModdingAPI.Framework // load game content if (!assetName.StartsWith(this.ModContentPrefix)) -#if STARDEW_VALLEY_1_3 return this.LoadImpl(assetName, instance, language); -#else - return this.LoadImpl(assetName, instance); -#endif // load mod content SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); @@ -235,11 +223,7 @@ namespace StardewModdingAPI.Framework { // try cache if (this.IsLoaded(assetName)) -#if STARDEW_VALLEY_1_3 return this.LoadImpl(assetName, instance, language); -#else - return this.LoadImpl(assetName, instance); -#endif // get file FileInfo file = this.GetModFile(assetName); @@ -251,11 +235,7 @@ namespace StardewModdingAPI.Framework { // XNB file case ".xnb": -#if STARDEW_VALLEY_1_3 return this.LoadImpl(assetName, instance, language); -#else - return this.LoadImpl(assetName, instance); -#endif // unpacked map case ".tbin": @@ -436,10 +416,6 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private IDictionary GetKeyLocales(Reflector reflection) { -#if !STARDEW_VALLEY_1_3 - IReflectedField codeField = reflection.GetField(typeof(LocalizedContentManager), "_currentLangCode"); - LocalizedContentManager.LanguageCode previousCode = codeField.GetValue(); -#endif string previousOverride = this.Content.LanguageCodeOverride; try @@ -448,21 +424,11 @@ namespace StardewModdingAPI.Framework this.Content.LanguageCodeOverride = null; // create locale => code map - IReflectedMethod languageCodeString = reflection -#if STARDEW_VALLEY_1_3 - .GetMethod(this.Content, "languageCodeString"); -#else - .GetMethod(this.Content, "languageCode"); -#endif + IReflectedMethod languageCodeString = reflection.GetMethod(this.Content, "languageCodeString"); IDictionary map = new Dictionary(); foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) { -#if STARDEW_VALLEY_1_3 map[code] = languageCodeString.Invoke(code); -#else - codeField.SetValue(code); - map[code] = languageCodeString.Invoke(); -#endif } return map; @@ -471,10 +437,6 @@ namespace StardewModdingAPI.Framework { // restore previous settings this.Content.LanguageCodeOverride = previousOverride; -#if !STARDEW_VALLEY_1_3 - codeField.SetValue(previousCode); -#endif - } } @@ -520,18 +482,12 @@ namespace StardewModdingAPI.Framework /// The normalised asset name. private bool IsNormalisedKeyLoaded(string normalisedAssetName) { -#if STARDEW_VALLEY_1_3 if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) return false; return localisable ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}") : this.Cache.ContainsKey(normalisedAssetName); -#else - return - this.Cache.ContainsKey(normalisedAssetName) - || this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}"); // translated asset -#endif } /// Track that a content manager loaded an asset. @@ -552,11 +508,7 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. /// The content manager instance for which to load the asset. /// The language code for which to load content. - private T LoadImpl(string assetName, ContentManager instance -#if STARDEW_VALLEY_1_3 - , LocalizedContentManager.LanguageCode language -#endif - ) + private T LoadImpl(string assetName, ContentManager instance, LocalizedContentManager.LanguageCode language) { return this.WithWriteLock(() => { @@ -564,13 +516,7 @@ namespace StardewModdingAPI.Framework if (this.IsNormalisedKeyLoaded(assetName)) { this.TrackAssetLoader(assetName, instance); - return this.Content - -#if STARDEW_VALLEY_1_3 - .Load(assetName, language); -#else - .Load(assetName); -#endif + return this.Content.Load(assetName, language); } // load asset @@ -579,30 +525,17 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = this.Content -#if STARDEW_VALLEY_1_3 - .Load(assetName, language); -#else - .Load(assetName); -#endif + data = this.Content.Load(assetName, language); } else { data = this.AssetsBeingLoaded.Track(assetName, () => { - string locale = -#if STARDEW_VALLEY_1_3 - this.GetLocale(language); -#else - this.GetLocale(); -#endif + string locale = this.GetLocale(language); IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) -#if STARDEW_VALLEY_1_3 + IAssetData asset = + this.ApplyLoader(info) ?? new AssetDataForObject(info, this.Content.Load(assetName, language), this.NormaliseAssetName); -#else - ?? new AssetDataForObject(info, this.Content.Load(assetName), this.NormaliseAssetName); -#endif asset = this.ApplyEditors(info, asset); return (T)asset.Data; }); diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs index 8f88fc2d..2791eb78 100644 --- a/src/SMAPI/Framework/ContentManagerShim.cs +++ b/src/SMAPI/Framework/ContentManagerShim.cs @@ -43,14 +43,9 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) { -#if STARDEW_VALLEY_1_3 return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); -#else - return this.ContentCore.Load(assetName, this); -#endif } -#if STARDEW_VALLEY_1_3 /// 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. @@ -67,7 +62,6 @@ namespace StardewModdingAPI.Framework { return this.Load(assetName, LanguageCode.en); } -#endif /// Inject an asset into the cache. /// The type of asset to inject. @@ -78,13 +72,11 @@ namespace StardewModdingAPI.Framework this.ContentCore.Inject(assetName, value, this); } -#if STARDEW_VALLEY_1_3 /// Create a new content manager for temporary use. public override LocalizedContentManager CreateTemporary() { return this.ContentCore.CreateContentManager("(temporary)"); } -#endif /********* diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index e5022212..85b2eef6 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -49,10 +49,8 @@ namespace StardewModdingAPI.Framework /// The game version string. private static string GetSemanticVersionString(string gameVersion) { -#if STARDEW_VALLEY_1_3 - if(gameVersion.StartsWith("1.3.0.")) + if (gameVersion.StartsWith("1.3.0.")) return new SemanticVersion(1, 3, 0, "alpha." + gameVersion.Substring("1.3.0.".Length)).ToString(); -#endif return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) ? semanticVersion @@ -63,10 +61,8 @@ namespace StardewModdingAPI.Framework /// The semantic version string. private static string GetGameVersionString(string semanticVersion) { - #if STARDEW_VALLEY_1_3 - if(semanticVersion.StartsWith("1.3-alpha.")) + if (semanticVersion.StartsWith("1.3-alpha.")) return "1.3.0." + semanticVersion.Substring("1.3-alpha.".Length); - #endif foreach (var mapping in GameVersion.VersionMap) { diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index e5bf47f6..648d6742 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -107,122 +107,6 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } -#if !STARDEW_VALLEY_1_3 - /**** - ** Obsolete - ****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field wrapper, or null if the field doesn't exist and is false. - [Obsolete] - public IPrivateField GetPrivateField(object obj, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - return (IPrivateField)this.GetField(obj, name, required); - } - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete] - public IPrivateField GetPrivateField(Type type, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - return (IPrivateField)this.GetField(type, name, required); - } - - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - [Obsolete] - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - return (IPrivateProperty)this.GetProperty(obj, name, required); - } - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - [Obsolete] - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - return (IPrivateProperty)this.GetProperty(type, name, required); - } - - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - [Obsolete] - public TValue GetPrivateValue(object obj, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - IPrivateField field = (IPrivateField)this.GetField(obj, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - [Obsolete] - public TValue GetPrivateValue(Type type, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - IPrivateField field = (IPrivateField)this.GetField(type, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete] - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - return (IPrivateMethod)this.GetMethod(obj, name, required); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete] - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) - { - this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); - return (IPrivateMethod)this.GetMethod(type, name, required); - } -#endif - /********* ** Private methods diff --git a/src/SMAPI/Framework/Reflection/ReflectedField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs index fb420dc5..09638b1d 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedField.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs @@ -6,9 +6,6 @@ namespace StardewModdingAPI.Framework.Reflection /// A field obtained through reflection. /// The field value type. internal class ReflectedField : IReflectedField -#if !STARDEW_VALLEY_1_3 - , IPrivateField -#endif { /********* ** Properties diff --git a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs index 803bc316..7d9072a0 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs @@ -5,9 +5,6 @@ namespace StardewModdingAPI.Framework.Reflection { /// A method obtained through reflection. internal class ReflectedMethod : IReflectedMethod -#if !STARDEW_VALLEY_1_3 - , IPrivateMethod -#endif { /********* ** Properties diff --git a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs index 4f9d4e19..d59b71ac 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs @@ -6,9 +6,6 @@ namespace StardewModdingAPI.Framework.Reflection /// A property obtained through reflection. /// The property value type. internal class ReflectedProperty : IReflectedProperty -#if !STARDEW_VALLEY_1_3 - , IPrivateProperty -#endif { /********* ** Properties diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c6e9aa92..b486f8cd 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,18 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; -#if STARDEW_VALLEY_1_3 using Netcode; -#endif using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; @@ -24,11 +20,7 @@ using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; -#if !STARDEW_VALLEY_1_3 -using xTile.Layers; -#else using SFarmer = StardewValley.Farmer; -#endif namespace StardewModdingAPI.Framework { @@ -136,11 +128,6 @@ namespace StardewModdingAPI.Framework /// Whether this is the very first update tick since the game started. private bool FirstUpdate; -#if !STARDEW_VALLEY_1_3 - /// The current game instance. - private static SGame Instance; -#endif - /// A callback to invoke after the game finishes initialising. private readonly Action OnGameInitialised; @@ -150,29 +137,9 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private static Reflector Reflection; -#if !STARDEW_VALLEY_1_3 // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming - /// Used to access private fields and methods. - private static List _fpsList => SGame.Reflection.GetField>(typeof(Game1), nameof(_fpsList)).GetValue(); - private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); - private static float _fps - { - set => SGame.Reflection.GetField(typeof(Game1), nameof(_fps)).SetValue(value); - } - private static Task _newDayTask => SGame.Reflection.GetField(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetField(this, nameof(this.bgColor)).GetValue(); - public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(this.lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); - private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke(); - private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); - // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming -#endif - -#if STARDEW_VALLEY_1_3 private static StringBuilder _debugStringBuilder => SGame.Reflection.GetField(typeof(Game1), nameof(_debugStringBuilder)).GetValue(); -#endif + // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /********* @@ -199,9 +166,6 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.Events = eventManager; this.FirstUpdate = true; -#if !STARDEW_VALLEY_1_3 - SGame.Instance = this; -#endif SGame.Reflection = reflection; this.OnGameInitialised = onGameInitialised; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case @@ -209,13 +173,6 @@ namespace StardewModdingAPI.Framework // set XNA option required by Stardew Valley Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - -#if !STARDEW_VALLEY_1_3 - // replace already-created content managers - this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); - this.Content = this.ContentCore.CreateContentManager("SGame.Content"); - reflection.GetField(typeof(Game1), "_temporaryContent").SetValue(this.ContentCore.CreateContentManager("Game1._temporaryContent")); // regenerate value with new content manager -#endif } /**** @@ -262,11 +219,7 @@ namespace StardewModdingAPI.Framework // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one // update tick are neglible and not worth the complications of bypassing Game1.Update. -#if STARDEW_VALLEY_1_3 if (Game1._newDayTask != null) -#else - if (SGame._newDayTask != null) -#endif { base.Update(gameTime); this.Events.Specialised_UnvalidatedUpdateTick.Raise(); @@ -553,13 +506,7 @@ namespace StardewModdingAPI.Framework // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged( -#if STARDEW_VALLEY_1_3 - Game1.currentLocation.objects.FieldDict -#else - Game1.currentLocation.objects -#endif - )); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects.FieldDict)); // raise time changed if (Game1.timeOfDay != this.PreviousTime) @@ -686,7 +633,6 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] -#if STARDEW_VALLEY_1_3 private void DrawImpl(GameTime gameTime) { if (Game1.debugMode) @@ -1318,659 +1264,6 @@ namespace StardewModdingAPI.Framework } } } -#else - private void DrawImpl(GameTime gameTime) - { - if (Game1.debugMode) - { - if (SGame._fpsStopwatch.IsRunning) - { - float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; - SGame._fpsList.Add(totalSeconds); - while (SGame._fpsList.Count >= 120) - SGame._fpsList.RemoveAt(0); - float num = 0.0f; - foreach (float fps in SGame._fpsList) - num += fps; - SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); - } - SGame._fpsStopwatch.Restart(); - } - else - { - if (SGame._fpsStopwatch.IsRunning) - SGame._fpsStopwatch.Reset(); - SGame._fps = 0.0f; - SGame._fpsList.Clear(); - } - if (SGame._newDayTask != null) - { - this.GraphicsDevice.Clear(this.bgColor); - //base.Draw(gameTime); - } - else - { - if ((double)Game1.options.zoomLevel != 1.0) - this.GraphicsDevice.SetRenderTarget(this.screenWrapper); - if (this.IsSaving) - { - this.GraphicsDevice.Clear(this.bgColor); - IClickableMenu activeClickableMenu = Game1.activeClickableMenu; - if (activeClickableMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); - activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - activeClickableMenu.exitThisMenu(); - } - this.RaisePostRender(); - Game1.spriteBatch.End(); - } - //base.Draw(gameTime); - this.renderScreenBuffer(); - } - else - { - this.GraphicsDevice.Clear(this.bgColor); - if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - this.RaisePostRender(); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - } - else if ((int)Game1.gameMode == 11) - { - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); - this.RaisePostRender(); - Game1.spriteBatch.End(); - } - else if (Game1.currentMinigame != null) - { - Game1.currentMinigame.draw(Game1.spriteBatch); - if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); - } - this.RaisePostRender(needsNewBatch: true); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - } - else if (Game1.showingEndOfNightStuff) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.activeClickableMenu != null) - { - try - { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - this.RaisePostRender(); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - } - else if ((int)Game1.gameMode == 6) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - string str1 = ""; - for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) - str1 += "."; - string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string str3 = str1; - string s = str2 + str3; - string str4 = "..."; - string str5 = str2 + str4; - int widthOfString = SpriteText.getWidthOfString(str5); - int height = 64; - int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; - SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); - this.RaisePostRender(); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - } - else - { - Microsoft.Xna.Framework.Rectangle rectangle; - if ((int)Game1.gameMode == 0) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - } - else - { - if (Game1.drawLighting) - { - this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Color.White * 0.0f); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight)); - for (int index = 0; index < Game1.currentLightSources.Count; ++index) - { - if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(index).position, (int)((double)Game1.currentLightSources.ElementAt(index).radius * (double)Game1.tileSize * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); - } - Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); - } - if (Game1.bloomDay && Game1.bloom != null) - Game1.bloom.BeginDraw(); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - this.Events.Graphics_OnPreRenderEvent.Raise(); - if (Game1.background != null) - Game1.background.draw(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.currentLocation.drawWater(Game1.spriteBatch); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); - } - } - Microsoft.Xna.Framework.Rectangle bounds; - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - } - } - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - rectangle = Game1.shadowTexture.Bounds; - double y = (double)rectangle.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.displayFarmer) - Game1.player.draw(Game1.spriteBatch); - if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0)); - Game1.currentLocation.draw(Game1.spriteBatch); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; - } - if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Name.Equals("Farm")) - this.drawFarmBuildings(); - if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); - foreach (Warp warp in Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U) - { - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - } - if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null) - { - if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) - { - Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size1 = Game1.viewport.Size; - if (layer1.PickTile(mapDisplayLocation1, size1) != null) - { - Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size2 = Game1.viewport.Size; - if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_127; - } - else - goto label_127; - } - Game1.drawPlayerHeldObject(Game1.player); - } - label_127: - if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) - { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - } - if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) - { - Color color = Color.White; - switch ((int)((double)Game1.toolHold / 600.0) + 2) - { - case 1: - color = Tool.copperColor; - break; - case 2: - color = Tool.steelColor; - break; - case 3: - color = Tool.goldColor; - break; - case 4: - color = Tool.iridiumColor; - break; - } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color); - } - if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) - Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize))))) - { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); - } - Game1.spriteBatch.End(); - //base.Draw(gameTime); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - foreach (NPC actor in Game1.currentLocation.currentEvent.actors) - { - if (actor.isEmoting) - { - Vector2 localPosition = actor.getLocalPosition(Game1.viewport); - localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3); - if (actor.age == 2) - localPosition.Y += (float)(Game1.tileSize / 2); - else if (actor.gender == 1) - localPosition.Y += (float)(Game1.tileSize / 6); - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); - } - } - } - Game1.spriteBatch.End(); - if (Game1.drawLighting) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); - if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.drawGrid) - { - int x1 = -Game1.viewport.X % Game1.tileSize; - float num1 = (float)(-Game1.viewport.Y % Game1.tileSize); - int x2 = x1; - while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); - x2 += Game1.tileSize; - } - float num2 = num1; - while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); - num2 += (float)Game1.tileSize; - } - } - if (Game1.currentBillboard != 0) - this.drawBillboard(); - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) - { - this.Events.Graphics_OnPreRenderHudEvent.Raise(); - this.drawHUD(); - this.Events.Graphics_OnPostRenderHudEvent.Raise(); - } - else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); - if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) - { - for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) - Game1.hudMessages[i].draw(Game1.spriteBatch, i); - } - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) - this.drawDialogueBox(); - Viewport viewport; - if (Game1.progressBar) - { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea; - int y1 = rectangle.Bottom - Game1.tileSize * 2; - int dialogueWidth = Game1.dialogueWidth; - int height1 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); - Color lightGray = Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - viewport = Game1.graphics.GraphicsDevice.Viewport; - rectangle = viewport.TitleSafeArea; - int y2 = rectangle.Bottom - Game1.tileSize * 2; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - int height2 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); - Color dimGray = Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); - } - if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Blue * 0.2f; - spriteBatch.Draw(staminaRect, bounds, color); - } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - else if ((double)Game1.flashAlpha > 0.0) - { - if (Game1.options.screenFlash) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.White * Math.Min(1f, Game1.flashAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - Game1.flashAlpha -= 0.1f; - } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) - this.drawDialogueBox(); - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0); - if (Game1.debugMode) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - SpriteFont smallFont = Game1.smallFont; - object[] objArray = new object[10]; - int index1 = 0; - string str1; - if (!Game1.panMode) - str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize); - else - str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize); - objArray[index1] = (object)str1; - int index2 = 1; - string str2 = " mouseTransparency: "; - objArray[index2] = (object)str2; - int index3 = 2; - float cursorTransparency = Game1.mouseCursorTransparency; - objArray[index3] = (object)cursorTransparency; - int index4 = 3; - string str3 = " mousePosition: "; - objArray[index4] = (object)str3; - int index5 = 4; - int mouseX = Game1.getMouseX(); - objArray[index5] = (object)mouseX; - int index6 = 5; - string str4 = ","; - objArray[index6] = (object)str4; - int index7 = 6; - int mouseY = Game1.getMouseY(); - objArray[index7] = (object)mouseY; - int index8 = 7; - string newLine = Environment.NewLine; - objArray[index8] = (object)newLine; - int index9 = 8; - string str5 = "debugOutput: "; - objArray[index9] = (object)str5; - int index10 = 9; - string debugOutput = Game1.debugOutput; - objArray[index10] = (object)debugOutput; - string text = string.Concat(objArray); - Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); - Color red = Color.Red; - double num1 = 0.0; - Vector2 zero = Vector2.Zero; - double num2 = 1.0; - int num3 = 0; - double num4 = 0.99999988079071; - spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.showKeyHelp) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null) - { - try - { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - else if (Game1.farmEvent != null) - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - - this.RaisePostRender(); - Game1.spriteBatch.End(); - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - this.renderScreenBuffer(); - } - } - } - } -#endif /**** ** Methods diff --git a/src/SMAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs deleted file mode 100644 index 42bf7d2e..00000000 --- a/src/SMAPI/IPrivateField.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if !STARDEW_VALLEY_1_3 -using System; -using System.Reflection; - -namespace StardewModdingAPI -{ - /// A private field obtained through reflection. - /// The field value type. - [Obsolete("Use " + nameof(IReflectedField) + " instead")] - public interface IPrivateField - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - FieldInfo FieldInfo { get; } - - - /********* - ** Public methods - *********/ - /// Get the field value. - TValue GetValue(); - - /// Set the field value. - //// The value to set. - void SetValue(TValue value); - } -} -#endif diff --git a/src/SMAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs deleted file mode 100644 index c24db602..00000000 --- a/src/SMAPI/IPrivateMethod.cs +++ /dev/null @@ -1,31 +0,0 @@ -#if !STARDEW_VALLEY_1_3 -using System; -using System.Reflection; - -namespace StardewModdingAPI -{ - /// A private method obtained through reflection. - [Obsolete("Use " + nameof(IReflectedMethod) + " instead")] - public interface IPrivateMethod - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - MethodInfo MethodInfo { get; } - - - /********* - ** Public methods - *********/ - /// Invoke the method. - /// The return type. - /// The method arguments to pass in. - TValue Invoke(params object[] arguments); - - /// Invoke the method. - /// The method arguments to pass in. - void Invoke(params object[] arguments); - } -} -#endif diff --git a/src/SMAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs deleted file mode 100644 index a1b21a69..00000000 --- a/src/SMAPI/IPrivateProperty.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if !STARDEW_VALLEY_1_3 -using System; -using System.Reflection; - -namespace StardewModdingAPI -{ - /// A private property obtained through reflection. - /// The property value type. - [Obsolete("Use " + nameof(IPrivateProperty) + " instead")] - public interface IPrivateProperty - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - PropertyInfo PropertyInfo { get; } - - - /********* - ** Public methods - *********/ - /// Get the property value. - TValue GetValue(); - - /// Set the property value. - //// The value to set. - void SetValue(TValue value); - } -} -#endif diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs index 60441471..a2b9eb32 100644 --- a/src/SMAPI/IReflectionHelper.cs +++ b/src/SMAPI/IReflectionHelper.cs @@ -47,74 +47,5 @@ namespace StardewModdingAPI /// The field name. /// Whether to throw an exception if the field is not found. IReflectedMethod GetMethod(Type type, string name, bool required = true); - -#if !STARDEW_VALLEY_1_3 - /***** - ** Obsolete - *****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] - IPrivateField GetPrivateField(object obj, string name, bool required = true); - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] - IPrivateField GetPrivateField(Type type, string name, bool required = true); - - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] - IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true); - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] - IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true); - - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// This is a shortcut for followed by . - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] - TValue GetPrivateValue(object obj, string name, bool required = true); - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// This is a shortcut for followed by . - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] - TValue GetPrivateValue(Type type, string name, bool required = true); - - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] - IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] - IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); -#endif } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index e54e0286..01ea24df 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -80,17 +80,8 @@ namespace StardewModdingAPI.Metadata ** Buildings ****/ case "buildings\\houses": // Farm -#if STARDEW_VALLEY_1_3 reflection.GetField(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load(key)); return true; -#else - { - Farm farm = Game1.getFarm(); - if (farm == null) - return false; - return farm.houseTextures = content.Load(key); - } -#endif /**** ** Content\Characters\Farmer @@ -101,20 +92,12 @@ namespace StardewModdingAPI.Metadata case "characters\\farmer\\farmer_base": // Farmer if (Game1.player == null || !Game1.player.isMale) return false; -#if STARDEW_VALLEY_1_3 return Game1.player.FarmerRenderer = new FarmerRenderer(key); -#else - return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); -#endif case "characters\\farmer\\farmer_girl_base": // Farmer if (Game1.player == null || Game1.player.isMale) return false; -#if STARDEW_VALLEY_1_3 return Game1.player.FarmerRenderer = new FarmerRenderer(key); -#else - return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); -#endif case "characters\\farmer\\hairstyles": // Game1.loadContent return FarmerRenderer.hairStylesTexture = content.Load(key); @@ -206,11 +189,6 @@ namespace StardewModdingAPI.Metadata /**** ** Content\Critters ****/ -#if !STARDEW_VALLEY_1_3 - case "tilesheets\\critters": // Criter.InitShared - return Critter.critterTexture = content.Load(key); -#endif - case "tilesheets\\crops": // Game1.loadContent return Game1.cropSpriteSheet = content.Load(key); @@ -265,11 +243,7 @@ namespace StardewModdingAPI.Metadata Texture2D texture = content.Load(key); reflection.GetField(titleMenu, "titleButtonsTexture").SetValue(texture); foreach (TemporaryAnimatedSprite bird in reflection.GetField>(titleMenu, "birds").GetValue()) -#if STARDEW_VALLEY_1_3 bird.texture = texture; -#else - bird.Texture = texture; -#endif return true; } return false; @@ -284,12 +258,8 @@ namespace StardewModdingAPI.Metadata return Game1.buffsIcons = content.Load(key); case "tilesheets\\bushes": // new Bush() -#if STARDEW_VALLEY_1_3 reflection.GetField>(typeof(Bush), "texture").SetValue(new Lazy(() => content.Load(key))); return true; -#else - return Bush.texture = content.Load(key); -#endif case "tilesheets\\craftables": // Game1.loadContent return Game1.bigCraftableSpriteSheet = content.Load(key); @@ -350,7 +320,7 @@ namespace StardewModdingAPI.Metadata return this.ReloadNpcSprites(content, key, monster: true); if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"), StringComparison.InvariantCultureIgnoreCase)) - return this.ReloadFenceTextures(content, key); + return this.ReloadFenceTextures(key); if (this.IsInFolder(key, "Portraits")) return this.ReloadNpcPortraits(content, key); @@ -435,21 +405,16 @@ namespace StardewModdingAPI.Metadata { Lazy texture = new Lazy(() => content.Load(key)); foreach (Building building in buildings) -#if STARDEW_VALLEY_1_3 building.texture = texture; -#else - building.texture = texture.Value; -#endif return true; } return false; } /// Reload the sprites for a fence type. - /// The content manager through which to reload the asset. /// The asset key to reload. /// Returns whether any textures were reloaded. - private bool ReloadFenceTextures(LocalizedContentManager content, string key) + private bool ReloadFenceTextures(string key) { // get fence type if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType)) @@ -528,11 +493,7 @@ namespace StardewModdingAPI.Metadata { Lazy texture = new Lazy(() => content.Load(key)); foreach (Tree tree in trees) -#if STARDEW_VALLEY_1_3 this.Reflection.GetField>(tree, "texture").SetValue(texture); -#else - this.Reflection.GetField(tree, "texture").SetValue(texture.Value); -#endif return true; } @@ -547,11 +508,7 @@ namespace StardewModdingAPI.Metadata /// The texture to set. private void SetSpriteTexture(AnimatedSprite sprite, Texture2D texture) { -#if STARDEW_VALLEY_1_3 this.Reflection.GetField(sprite, "spriteTexture").SetValue(texture); -#else - sprite.Texture = texture; -#endif } /// Get an NPC name from the name of their file under Content/Characters. diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 4960a458..0b532a18 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -6,9 +6,6 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; using StardewValley; -#if STARDEW_VALLEY_1_3 -using SObject = StardewValley.Object; -#endif namespace StardewModdingAPI.Metadata { @@ -37,75 +34,15 @@ namespace StardewModdingAPI.Metadata // rewrite for crossplatform compatibility new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), -#if !STARDEW_VALLEY_1_3 - // rewrite for Stardew Valley 1.2 - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), - new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), - new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), - - // rewrite for SMAPI 1.9 - new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), -#endif - // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), // rewrite for Stardew Valley 1.3 -#if STARDEW_VALLEY_1_3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), -#endif /**** ** detect incompatible code ****/ - #if !STARDEW_VALLEY_1_3 - // detect changes in Stardew Valley 1.2 - new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible), - - // detect APIs removed in SMAPI 1.9 - new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Extensions", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Inheritance.SGame", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Inheritance.SObject", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.LogWriter", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Manifest", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Version", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), - - // detect APIs removed in SMAPI 2.0 - new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), - #endif - // detect broken code new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies), new ReferenceToMemberWithUnexpectedTypeFinder(this.ValidateReferencesToAssemblies), diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index edddbd2a..cf476193 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -231,7 +231,6 @@ - @@ -248,8 +247,6 @@ - - -- cgit From 5997857064f4d6bb0747e84e1dbd1556c97b7481 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Apr 2018 00:18:32 -0400 Subject: fix various net field conversions in SMAPI code (#453) --- src/SMAPI/Context.cs | 2 +- src/SMAPI/Framework/SGame.cs | 7 ++++--- src/SMAPI/Metadata/CoreAssetPropagator.cs | 27 ++++++++++++++------------- src/SMAPI/StardewModdingAPI.csproj | 3 +++ 4 files changed, 22 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index 7ed9b052..79067b2d 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI ** Internal ****/ /// Whether a player save has been loaded. - internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.Name); /// Whether the game is currently writing to the save file. internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b486f8cd..67390882 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -500,9 +500,9 @@ namespace StardewModdingAPI.Framework this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); // raise player inventory changed - ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); + ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.Items, this.PreviousItems).ToArray(); if (changedItems.Any()) - this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.items, changedItems.ToList())); + this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) @@ -526,7 +526,7 @@ namespace StardewModdingAPI.Framework this.PreviousForagingLevel = Game1.player.foragingLevel; this.PreviousMiningLevel = Game1.player.miningLevel; this.PreviousLuckLevel = Game1.player.luckLevel; - this.PreviousItems = Game1.player.items.Where(n => n != null).Distinct().ToDictionary(n => n, n => n.Stack); + this.PreviousItems = Game1.player.Items.Where(n => n != null).Distinct().ToDictionary(n => n, n => n.Stack); this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); this.PreviousTime = Game1.timeOfDay; this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; @@ -633,6 +633,7 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "SMAPI002", Justification = "copied from game code as-is")] private void DrawImpl(GameTime gameTime) { if (Game1.debugMode) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 01ea24df..37623f32 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -90,12 +90,12 @@ namespace StardewModdingAPI.Metadata return FarmerRenderer.accessoriesTexture = content.Load(key); case "characters\\farmer\\farmer_base": // Farmer - if (Game1.player == null || !Game1.player.isMale) + if (Game1.player == null || !Game1.player.IsMale) return false; return Game1.player.FarmerRenderer = new FarmerRenderer(key); case "characters\\farmer\\farmer_girl_base": // Farmer - if (Game1.player == null || Game1.player.isMale) + if (Game1.player == null || Game1.player.IsMale) return false; return Game1.player.FarmerRenderer = new FarmerRenderer(key); @@ -372,16 +372,16 @@ namespace StardewModdingAPI.Metadata foreach (FarmAnimal animal in animals) { // get expected key - string expectedKey = animal.age < animal.ageWhenMature - ? $"Baby{(animal.type == "Duck" ? "White Chicken" : animal.type)}" + string expectedKey = animal.age.Value < animal.ageWhenMature.Value + ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}" : animal.type; - if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce <= 0) + if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce.Value <= 0) expectedKey = $"Sheared{expectedKey}"; expectedKey = $"Animals\\{expectedKey}"; // reload asset if (expectedKey == key) - this.SetSpriteTexture(animal.sprite, texture.Value); + this.SetSpriteTexture(animal.Sprite, texture.Value); } return texture.IsValueCreated; } @@ -397,7 +397,7 @@ namespace StardewModdingAPI.Metadata Building[] buildings = Game1.locations .OfType() .SelectMany(p => p.buildings) - .Where(p => p.buildingType == type) + .Where(p => p.buildingType.Value == type) .ToArray(); // reload buildings @@ -427,7 +427,7 @@ namespace StardewModdingAPI.Metadata from fence in location.Objects.Values.OfType() where fenceType == 1 ? fence.isGate - : fence.whichType == fenceType + : fence.whichType.Value == fenceType select fence ) .ToArray(); @@ -447,7 +447,7 @@ namespace StardewModdingAPI.Metadata { // get NPCs string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); - NPC[] characters = this.GetCharacters().Where(npc => npc.name == name && npc.IsMonster == monster).ToArray(); + NPC[] characters = this.GetCharacters().Where(npc => npc.Name == name && npc.IsMonster == monster).ToArray(); if (!characters.Any()) return false; @@ -466,7 +466,7 @@ namespace StardewModdingAPI.Metadata { // get NPCs string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); - NPC[] villagers = this.GetCharacters().Where(npc => npc.name == name && npc.isVillager()).ToArray(); + NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -486,7 +486,7 @@ namespace StardewModdingAPI.Metadata { Tree[] trees = Game1.locations .SelectMany(p => p.terrainFeatures.Values.OfType()) - .Where(tree => tree.treeType == type) + .Where(tree => tree.treeType.Value == type) .ToArray(); if (trees.Any()) @@ -562,8 +562,9 @@ namespace StardewModdingAPI.Metadata { foreach (Building building in buildableLocation.buildings) { - if (building.indoors != null) - yield return building.indoors; + GameLocation indoors = building.indoors; + if (indoors != null) + yield return indoors; } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index cf476193..c0d5386a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -288,6 +288,9 @@ StardewModdingAPI.AssemblyRewriters + + + \ No newline at end of file -- cgit From a3ade7a5126642f42794281057349fa5ff737cdd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 22:41:34 -0400 Subject: split mod DB into a separate file The mod metadata has grown over time, and there's no need to keep it in memory after mod loading. This lets us load the config earlier (since it has a smaller impact on memory usage which affects the game's audio code), and lets us discard the mod metadata when we're done with it. --- build/common.targets | 1 + build/prepare-install-package.targets | 2 + docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 3 + src/SMAPI/Framework/Models/SConfig.cs | 6 - src/SMAPI/Framework/Models/SMetadata.cs | 15 + src/SMAPI/Program.cs | 23 +- src/SMAPI/StardewModdingAPI.config.json | 1837 +---------------------------- src/SMAPI/StardewModdingAPI.csproj | 4 + src/SMAPI/StardewModdingAPI.metadata.json | 1836 ++++++++++++++++++++++++++++ 10 files changed, 1875 insertions(+), 1853 deletions(-) create mode 100644 src/SMAPI/Framework/Models/SMetadata.cs create mode 100644 src/SMAPI/StardewModdingAPI.metadata.json (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 1230ec15..a6cead8f 100644 --- a/build/common.targets +++ b/build/common.targets @@ -87,6 +87,7 @@ + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index fca311f2..348cc1bc 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -31,6 +31,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/docs/release-notes.md b/docs/release-notes.md index e68720da..f0202ee1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ * For SMAPI developers: * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. + * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. --> ## 2.5.5 diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index e547dfa6..a098194b 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -72,6 +72,9 @@ namespace StardewModdingAPI /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + /// The file path for the SMAPI metadata file. + internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); + /// The file path to the log where the latest output should be saved. internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 17169714..2d6da0fa 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using StardewModdingAPI.Framework.ModData; - namespace StardewModdingAPI.Framework.Models { /// The SMAPI configuration settings. @@ -23,8 +20,5 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } - - /// Extra metadata about mods. - public IDictionary ModData { get; set; } } } diff --git a/src/SMAPI/Framework/Models/SMetadata.cs b/src/SMAPI/Framework/Models/SMetadata.cs new file mode 100644 index 00000000..9ff495e9 --- /dev/null +++ b/src/SMAPI/Framework/Models/SMetadata.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.ModData; + +namespace StardewModdingAPI.Framework.Models +{ + /// The SMAPI predefined metadata. + internal class SMetadata + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ff4e9a50..da2c0e8e 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -54,16 +54,15 @@ namespace StardewModdingAPI /// Simplifies access to private game code. private readonly Reflector Reflection = new Reflector(); + /// The SMAPI configuration settings. + private readonly SConfig Settings; + /// The underlying game instance. private SGame GameInstance; /// The underlying content manager. private ContentCore ContentCore => this.GameInstance.ContentCore; - /// The SMAPI configuration settings. - /// This is initialised after the game starts. - private SConfig Settings; - /// Tracks the installed mods. /// This is initialised after the game starts. private readonly ModRegistry ModRegistry = new ModRegistry(); @@ -133,8 +132,14 @@ namespace StardewModdingAPI public Program(bool writeToConsole, string logPath) { // init basics + this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + { + WriteToConsole = writeToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; this.EventManager = new EventManager(this.Monitor, this.ModRegistry); // hook up events @@ -345,7 +350,6 @@ namespace StardewModdingAPI private void InitialiseAfterGameStart() { // load settings - this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components @@ -361,11 +365,7 @@ namespace StardewModdingAPI // add headers if (this.Settings.DeveloperMode) - { - this.Monitor.ShowTraceInConsole = true; - this.Monitor.ShowFullStampInConsole = true; this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - } if (!this.Settings.CheckForUpdates) this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) @@ -377,7 +377,8 @@ namespace StardewModdingAPI this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); // load mod data - ModDatabase modDatabase = new ModDatabase(this.Settings.ModData, Constants.GetUpdateUrl); + SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiMetadataPath)); + ModDatabase modDatabase = new ModDatabase(metadata.ModData, Constants.GetUpdateUrl); // load mods { diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index f7082e96..9743894a 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -36,1840 +36,5 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * Whether SMAPI should log more information about the game context. */ - "VerboseLogging": false, - - /** - * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This - * field shouldn't be edited by players in most cases. - * - * Standard fields - * =============== - * The predefined fields are documented below (only 'ID' is required). Each entry's key is the - * default display name for the mod if one isn't available (e.g. in dependency checks). - * - * - ID: the mod's latest unique ID (if any). - * - * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching - * other fields if no ID was specified. This doesn't include the latest ID, if any. - * Format rules: - * 1. If the mod's ID changed over time, multiple variants can be separated by the '|' - * character. - * 2. Each variant can take one of two forms: - * - A simple string matching the mod's UniqueID value. - * - A JSON structure containing any of four manifest fields (ID, Name, Author, and - * EntryDll) to match. - * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * - * Versioned metadata - * ================== - * Each record can also specify extra metadata using the field keys below. - * - * Each key consists of a field name prefixed with any combination of version range and 'Default', - * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, - * 'Default | UpdateKey' will only override if the mod has no update keys, and - * '~1.1 | Default | Name' will do the same up to version 1.1. - * - * The version format is 'min~max' (where either side can be blank for unbounded), or a single - * version number. - * - * These are the valid field names: - * - * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update - * checks for older mods that haven't been updated to use it yet. - * - * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load - * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it - * even if it detects incompatible code). - * - * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded - * (if applicable). If blank, will default to a generic not-compatible message. - * - * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the - * mod is no longer compatible. - */ - "ModData": { - "AccessChestAnywhere": { - "ID": "AccessChestAnywhere", - "MapLocalVersions": { "1.1-1078": "1.1" }, - "Default | UpdateKey": "Nexus:257", - "~1.1 | Status": "AssumeBroken" - }, - - "AdjustArtisanPrices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" }, - "Default | UpdateKey": "Chucklefish:3532", - "~0.0.1 | Status": "AssumeBroken" - }, - - "Adjust Monster": { - "ID": "mmanlapat.AdjustMonster", - "Default | UpdateKey": "Nexus:1161" - }, - - "Advanced Location Loader": { - "ID": "Entoarox.AdvancedLocationLoader", - "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) - "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Adventure Shop Inventory": { - "ID": "HammurabiAdventureShopInventory", - "Default | UpdateKey": "Chucklefish:4608" - }, - - "AgingMod": { - "ID": "skn.AgingMod", - "Default | UpdateKey": "Nexus:1129", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "All Crops All Seasons": { - "ID": "cantorsdust.AllCropsAllSeasons", - "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 - "Default | UpdateKey": "Nexus:170" - }, - - "All Professions": { - "ID": "cantorsdust.AllProfessions", - "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:174" - }, - - "Almighty Tool": { - "ID": "439", - "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 - "MapRemoteVersions": { "1.21": "1.2.1" }, - "Default | UpdateKey": "Nexus:439", - "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Animal Husbandry": { - "ID": "DIGUS.ANIMALHUSBANDRYMOD", - "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 - "Default | UpdateKey": "Nexus:1538" - }, - - "Animal Mood Fix": { - "ID": "GPeters-AnimalMoodFix", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - - "Animal Sitter": { - "ID": "jwdred.AnimalSitter", - "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 - "Default | UpdateKey": "Nexus:581", - "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ashley Mod": { - "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "A Tapper's Dream": { - "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "Default | UpdateKey": "Nexus:260", - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Auto Animal Doors": { - "ID": "AaronTaggart.AutoAnimalDoors", - "Default | UpdateKey": "Nexus:1019" - }, - - "Auto-Eat": { - "ID": "Permamiss.AutoEat", - "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 - "Default | UpdateKey": "Nexus:643" - }, - - "AutoFish": { - "ID": "WhiteMind.AF", - "Default | UpdateKey": "Nexus:1895" - }, - - "AutoGate": { - "ID": "AutoGate", - "Default | UpdateKey": "Nexus:820" - }, - - "Automate": { - "ID": "Pathoschild.Automate", - "Default | UpdateKey": "Nexus:1063" - }, - - "Automated Doors": { - "ID": "azah.automated-doors", - "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 - "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 - }, - - "AutoSpeed": { - "ID": "Omegasis.AutoSpeed", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:443" // added in 1.4.1 - }, - - "Basic Sprinklers Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:833" - }, - - "Better Hay": { - "ID": "cat.betterhay", - "Default | UpdateKey": "Nexus:1430" - }, - - "Better Quality More Seasons": { - "ID": "SB_BQMS", - "Default | UpdateKey": "Nexus:935" - }, - - "Better Quarry": { - "ID": "BetterQuarry", - "Default | UpdateKey": "Nexus:771" - }, - - "Better Ranching": { - "ID": "BetterRanching", - "Default | UpdateKey": "Nexus:859" - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" }, - "Default | UpdateKey": "Chucklefish:4302", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Better Sprinklers": { - "ID": "Speeder.BetterSprinklers", - "FormerIDs": "SPDSprinklersMod", // changed in 2.3 - "Default | UpdateKey": "Nexus:41", - "~2.3.1-pathoschild-update | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Billboard Anywhere": { - "ID": "Omegasis.BillboardAnywhere", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:492" // added in 1.4.1 - }, - - "Birthday Mail": { - "ID": "KathrynHazuka.BirthdayMail", - "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update - "Default | UpdateKey": "Nexus:276", - "~1.2.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Breed Like Rabbits": { - "ID": "dycedarger.breedlikerabbits", - "Default | UpdateKey": "Nexus:948" - }, - - "Build Endurance": { - "ID": "Omegasis.BuildEndurance", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:445", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Build Health": { - "ID": "Omegasis.BuildHealth", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:446", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Buy Cooking Recipes": { - "ID": "Denifia.BuyRecipes", - "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Buy Back Collectables": { - "ID": "Omegasis.BuyBackCollectables", - "FormerIDs": "BuyBackCollectables", // changed in 1.4 - "Default | UpdateKey": "Nexus:507", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Carry Chest": { - "ID": "spacechase0.CarryChest", - "Default | UpdateKey": "Nexus:1333" - }, - - "Casks Anywhere": { - "ID": "CasksAnywhere", - "MapLocalVersions": { "1.1-alpha": "1.1" }, - "Default | UpdateKey": "Nexus:878" - }, - - "Categorize Chests": { - "ID": "CategorizeChests", - "Default | UpdateKey": "Nexus:1300" - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" }, - "Default | UpdateKey": "Nexus:1030" - }, - - "Chest Label System": { - "ID": "Speeder.ChestLabel", - "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update - "Default | UpdateKey": "Nexus:242", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "Chest Pooling": { - "ID": "mralbobo.ChestPooling", - "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 - "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Chests Anywhere": { - "ID": "Pathoschild.ChestsAnywhere", - "FormerIDs": "ChestsAnywhere", // changed in 1.9 - "Default | UpdateKey": "Nexus:518", - "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Choose Baby Gender": { - "FormerIDs": "{EntryDll: 'ChooseBabyGender.dll'}", - "Default | UpdateKey": "Nexus:590", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "CJB Automation": { - "ID": "CJBAutomation", - "Default | UpdateKey": "Nexus:211", - "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" - }, - - "CJB Cheats Menu": { - "ID": "CJBok.CheatsMenu", - "FormerIDs": "CJBCheatsMenu", // changed in 1.14 - "Default | UpdateKey": "Nexus:4", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "CJB Item Spawner": { - "ID": "CJBok.ItemSpawner", - "FormerIDs": "CJBItemSpawner", // changed in 1.7 - "Default | UpdateKey": "Nexus:93", - "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "CJB Show Item Sell Price": { - "ID": "CJBok.ShowItemSellPrice", - "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 - "Default | UpdateKey": "Nexus:5", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Clean Farm": { - "ID": "tstaples.CleanFarm", - "Default | UpdateKey": "Nexus:794" - }, - - "Climates of Ferngill": { - "ID": "KoihimeNakamura.ClimatesOfFerngill", - "Default | UpdateKey": "Nexus:604", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Coal Regen": { - "ID": "Blucifer.CoalRegen", - "Default | UpdateKey": "Nexus:1664" - }, - - "Cold Weather Haley": { - "ID": "LordXamon.ColdWeatherHaleyPRO", - "Default | UpdateKey": "Nexus:1169", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Colored Chests": { - "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - - "Combat with Farm Implements": { - "ID": "SPDFarmingImplementsInCombat", - "Default | UpdateKey": "Nexus:313", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Community Bundle Item Tooltip": { - "ID": "musbah.bundleTooltip", - "Default | UpdateKey": "Nexus:1329" - }, - - "Concentration on Farming": { - "ID": "punyo.ConcentrationOnFarming", - "Default | UpdateKey": "Nexus:1445" - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" }, - "Default | UpdateKey": "Nexus:280" - }, - - "Configurable Shipping Dates": { - "ID": "ConfigurableShippingDates", - "Default | UpdateKey": "Nexus:675", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Cooking Skill": { - "ID": "spacechase0.CookingSkill", - "FormerIDs": "CookingSkill", // changed in 1.0.4–6 - "Default | UpdateKey": "Nexus:522", - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "CrabNet": { - "ID": "jwdred.CrabNet", - "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 - "Default | UpdateKey": "Nexus:584", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "Default | UpdateKey": "Nexus:1585" - }, - - "Current Location": { - "ID": "CurrentLocation102120161203", - "Default | UpdateKey": "Nexus:638" - }, - - "Custom Asset Modifier": { - "ID": "Omegasis.CustomAssetModifier", - "Default | UpdateKey": "1836" - }, - - "Custom Critters": { - "ID": "spacechase0.CustomCritters", - "Default | UpdateKey": "Nexus:1255" - }, - - "Custom Crops": { - "ID": "spacechase0.CustomCrops", - "Default | UpdateKey": "Nexus:1592" - }, - - "Custom Element Handler": { - "ID": "Platonymous.CustomElementHandler", - "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 - }, - - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991" // added in 0.6.1 - }, - - "Custom Farming Automate Bridge": { - "ID": "Platonymous.CFAutomate", - "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate - "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" - }, - - "Custom Farm Types": { - "ID": "spacechase0.CustomFarmTypes", - "Default | UpdateKey": "Nexus:1140" - }, - - "Custom Furniture": { - "ID": "Platonymous.CustomFurniture", - "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 - }, - - "Customize Exterior": { - "ID": "spacechase0.CustomizeExterior", - "FormerIDs": "CustomizeExterior", // changed in 1.0.3 - "Default | UpdateKey": "Nexus:1099", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Customizable Cart Redux": { - "ID": "KoihimeNakamura.CCR", - "MapLocalVersions": { "1.1-20170917": "1.1" }, - "Default | UpdateKey": "Nexus:1402" - }, - - "Customizable Traveling Cart Days": { - "ID": "TravelingCartYyeahdude", - "Default | UpdateKey": "Nexus:567", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1027" - }, - - "Custom NPC": { - "ID": "Platonymous.CustomNPC", - "Default | UpdateKey": "Nexus:1607" - }, - - "Custom Shops Redux": { - "ID": "Omegasis.CustomShopReduxGui", - "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 - }, - - "Custom TV": { - "ID": "Platonymous.CustomTV", - "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 - }, - - "Daily Luck Message": { - "ID": "Schematix.DailyLuckMessage", - "Default | UpdateKey": "Nexus:1327" - }, - - "Daily News": { - "ID": "bashNinja.DailyNews", - "Default | UpdateKey": "Nexus:1141", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Daily Quest Anywhere": { - "ID": "Omegasis.DailyQuestAnywhere", - "FormerIDs": "DailyQuest", // changed in 1.4 - "Default | UpdateKey": "Nexus:513" // added in 1.4.1 - }, - - "Debug Mode": { - "ID": "Pathoschild.DebugMode", - "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 - "Default | UpdateKey": "Nexus:679" - }, - - "Did You Water Your Crops?": { - "ID": "Nishtra.DidYouWaterYourCrops", - "Default | UpdateKey": "Nexus:1583" - }, - - "Dynamic Checklist": { - "ID": "gunnargolf.DynamicChecklist", - "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:874" - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" }, - "Default | UpdateKey": "Nexus:374", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Dynamic NPC Sprites": { - "ID": "BashNinja.DynamicNPCSprites", - "Default | UpdateKey": "Nexus:1183" - }, - - "Easier Farming": { - "ID": "cautiouswafffle.EasierFarming", - "Default | UpdateKey": "Nexus:1426" - }, - - "Empty Hands": { - "ID": "QuicksilverFox.EmptyHands", - "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Enemy Health Bars": { - "ID": "Speeder.HealthBars", - "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update - "Default | UpdateKey": "Nexus:193", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Entoarox Framework": { - "ID": "Entoarox.EntoaroxFramework", - "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? - "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) - "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) - }, - - "Expanded Fridge": { - "ID": "Uwazouri.ExpandedFridge", - "Default | UpdateKey": "Nexus:1191" - }, - - "Experience Bars": { - "ID": "spacechase0.ExperienceBars", - "FormerIDs": "ExperienceBars", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:509" - }, - - "Extended Bus System": { - "ID": "ExtendedBusSystem", - "Default | UpdateKey": "Chucklefish:4373" - }, - - "Extended Fridge": { - "ID": "Crystalmir.ExtendedFridge", - "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 - "Default | UpdateKey": "Nexus:485", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Extended Greenhouse": { - "ID": "ExtendedGreenhouse", - "Default | UpdateKey": "Chucklefish:4303", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Extended Minecart": { - "ID": "Entoarox.ExtendedMinecart", - "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 - "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) - }, - - "Extended Reach": { - "ID": "spacechase0.ExtendedReach", - "Default | UpdateKey": "Nexus:1493" - }, - - "Fall 28 Snow Day": { - "ID": "Omegasis.Fall28SnowDay", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:486", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Automation: Barn Door Automation": { - "FormerIDs": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Automation: Item Collector": { - "FormerIDs": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Farm Automation Unofficial: Item Collector": { - "ID": "Maddy99.FarmAutomation.ItemCollector", - "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Expansion": { - "ID": "Advize.FarmExpansion", - "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 - "Default | UpdateKey": "Nexus:130", - "~2.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Resource Generator": { - "FormerIDs": "{EntryDll: 'FarmResourceGenerator.dll'}", - "Default | UpdateKey": "Nexus:647", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Fast Animations": { - "ID": "Pathoschild.FastAnimations", - "Default | UpdateKey": "Nexus:1089" - }, - - "Faster Grass": { - "ID": "IceGladiador.FasterGrass", - "Default | UpdateKey": "Nexus:1772" - }, - - "Faster Paths": { - "ID": "Entoarox.FasterPaths", - "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander - "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) - }, - - "Faster Run": { - "ID": "KathrynHazuka.FasterRun", - "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update - "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Fishing Adjust": { - "ID": "shuaiz.FishingAdjustMod", - "Default | UpdateKey": "Nexus:1350" - }, - - "Fishing Tuner Redux": { - "ID": "HammurabiFishingTunerRedux", - "Default | UpdateKey": "Chucklefish:4578" - }, - - "Fixed Secret Woods Debris": { - "ID": "f4iTh.WoodsDebrisFix", - "Default | UpdateKey": "Nexus:1941" - }, - - "FlorenceMod": { - "FormerIDs": "{EntryDll: 'FlorenceMod.dll'}", - "MapLocalVersions": { "1.0.1": "1.1" }, - "Default | UpdateKey": "Nexus:591", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Flower Color Picker": { - "ID": "spacechase0.FlowerColorPicker", - "Default | UpdateKey": "Nexus:1229" - }, - - "Forage at the Farm": { - "ID": "Nishtra.ForageAtTheFarm", - "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 - "Default | UpdateKey": "Nexus:673", - "~1.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Furniture Anywhere": { - "ID": "Entoarox.FurnitureAnywhere", - "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart - "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) - }, - - "Game Reminder": { - "ID": "mmanlapat.GameReminder", - "Default | UpdateKey": "Nexus:1153" - }, - - "Gate Opener": { - "ID": "mralbobo.GateOpener", - "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 - "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "GenericShopExtender": { - "ID": "GenericShopExtender", - "Default | UpdateKey": "Nexus:814", // added in 0.1.3 - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Geode Info Menu": { - "ID": "cat.geodeinfomenu", - "Default | UpdateKey": "Nexus:1448" - }, - - "Get Dressed": { - "ID": "Advize.GetDressed", - "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 - "Default | UpdateKey": "Nexus:331", - "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Giant Crop Ring": { - "ID": "cat.giantcropring", - "Default | UpdateKey": "Nexus:1182" - }, - - "Gift Taste Helper": { - "ID": "tstaples.GiftTasteHelper", - "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 - "Default | UpdateKey": "Nexus:229", - "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Grandfather's Gift": { - "ID": "ShadowDragon.GrandfathersGift", - "Default | UpdateKey": "Nexus:985" - }, - - "Happy Animals": { - "ID": "HappyAnimals", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Happy Birthday (Omegasis)": { - "ID": "Omegasis.HappyBirthday", - "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:520", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Happy Birthday (Oxyligen fork)": { - "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:1064" // missing key reported: https://www.nexusmods.com/stardewvalley/mods/1064?tab=bugs - }, - - "Hardcore Mines": { - "ID": "kibbe.hardcore_mines", - "Default | UpdateKey": "Nexus:1674" - }, - - "Harp of Yoba Redux": { - "ID": "Platonymous.HarpOfYobaRedux", - "Default | UpdateKey": "Nexus:914" // added in 2.0.3 - }, - - "Harvest Moon Witch Princess": { - "ID": "Sasara.WitchPrincess", - "Default | UpdateKey": "Nexus:1157" - }, - - "Harvest With Scythe": { - "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "Default | UpdateKey": "Nexus:236", - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Horse Whistle (icepuente)": { - "ID": "icepuente.HorseWhistle", - "Default | UpdateKey": "Nexus:1131" - }, - - "Hunger (Yyeadude)": { - "ID": "HungerYyeadude", - "Default | UpdateKey": "Nexus:613" - }, - - "Hunger for Food (Tigerle)": { - "ID": "HungerForFoodByTigerle", - "Default | UpdateKey": "Nexus:810", - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Hunger Mod (skn)": { - "ID": "skn.HungerMod", - "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1127" - }, - - "Idle Pause": { - "ID": "Veleek.IdlePause", - "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:1092" - }, - - "Improved Quality of Life": { - "ID": "Demiacle.ImprovedQualityOfLife", - "Default | UpdateKey": "Nexus:1025", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Instant Geode": { - "ID": "InstantGeode", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Instant Grow Trees": { - "ID": "cantorsdust.InstantGrowTrees", - "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:173" - }, - - "Interaction Helper": { - "ID": "HammurabiInteractionHelper", - "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Item Auto Stacker": { - "ID": "cat.autostacker", - "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1184" - }, - - "Jiggly Junimo Bundles": { - "ID": "Greger.JigglyJunimoBundles", - "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update - "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update - }, - - "Json Assets": { - "ID": "spacechase0.JsonAssets", - "Default | UpdateKey": "Nexus:1720" - }, - - "Junimo Farm": { - "ID": "Platonymous.JunimoFarm", - "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:984" // added in 1.1.3 - }, - - "Less Strict Over-Exertion (AntiExhaustion)": { - "ID": "BALANCEMOD_AntiExhaustion", - "MapLocalVersions": { "0.0": "1.1" }, - "Default | UpdateKey": "Nexus:637", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Level Extender": { - "ID": "Devin Lematty.Level Extender", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1471" - }, - - "Level Up Notifications": { - "ID": "Level Up Notifications", - "MapRemoteVersions": { "0.0.1a": "0.0.1" }, - "Default | UpdateKey": "Nexus:855" - }, - - "Location and Music Logging": { - "ID": "Brandy Lover.LMlog", - "Default | UpdateKey": "Nexus:1366" - }, - - "Longevity": { - "ID": "RTGOAT.Longevity", - "MapRemoteVersions": { "1.6.8h": "1.6.8" }, - "Default | UpdateKey": "Nexus:649" - }, - - "Lookup Anything": { - "ID": "Pathoschild.LookupAnything", - "FormerIDs": "LookupAnything", // changed in 1.10.1 - "Default | UpdateKey": "Nexus:541", - "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Love Bubbles": { - "ID": "LoveBubbles", - "Default | UpdateKey": "Nexus:1318" - }, - - "Loved Labels": { - "ID": "Advize.LovedLabels", - "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 - "Default | UpdateKey": "Nexus:279", - "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Luck Skill": { - "ID": "spacechase0.LuckSkill", - "FormerIDs": "LuckSkill", // changed in 0.1.4 - "Default | UpdateKey": "Nexus:521", - "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mail Framework": { - "ID": "DIGUS.MailFrameworkMod", - "Default | UpdateKey": "Nexus:1536" - }, - - "MailOrderPigs": { - "ID": "jwdred.MailOrderPigs", - "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:632", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Makeshift Multiplayer": { - "ID": "spacechase0.StardewValleyMP", - "FormerIDs": "StardewValleyMP", // changed in 0.3 - "Default | UpdateKey": "Nexus:501", - "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Map Image Exporter": { - "ID": "spacechase0.MapImageExporter", - "FormerIDs": "MapImageExporter", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:1073" - }, - - "Message Box [API]? (ChatMod)": { - "ID": "Kithio:ChatMod", - "Default | UpdateKey": "Chucklefish:4296", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mining at the Farm": { - "ID": "Nishtra.MiningAtTheFarm", - "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 - "Default | UpdateKey": "Nexus:674" - }, - - "Mining With Explosives": { - "ID": "Nishtra.MiningWithExplosives", - "FormerIDs": "MiningWithExplosives", // changed in 1.1 - "Default | UpdateKey": "Nexus:770" - }, - - "Modder Serialization Utility": { - "ID": "SerializerUtils-0-1", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "Monster Level Tip": { - "ID": "WhiteMind.MonsterLT", - "Default | UpdateKey": "Nexus:1896" - }, - - "More Animals": { - "ID": "Entoarox.MoreAnimals", - "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 - "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) - "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility - }, - - "More Artifact Spots": { - "ID": "451", - "Default | UpdateKey": "Nexus:451", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "More Map Layers": { - "ID": "Platonymous.MoreMapLayers", - "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 - }, - - "More Rain": { - "ID": "Omegasis.MoreRain", - "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:441", // added in 1.5.1 - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "More Weapons": { - "ID": "Joco80.MoreWeapons", - "Default | UpdateKey": "Nexus:1168" - }, - - "Move Faster": { - "ID": "shuaiz.MoveFasterMod", - "Default | UpdateKey": "Nexus:1351" - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" }, - "Default | UpdateKey": "Nexus:1094", - "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Museum Rearranger": { - "ID": "Omegasis.MuseumRearranger", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:428", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mushroom Level Tip": { - "ID": "WhiteMind.MLT", - "Default | UpdateKey": "Nexus:1894" - }, - - "New Machines": { - "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "Default | UpdateKey": "Chucklefish:3683", - "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Night Owl": { - "ID": "Omegasis.NightOwl", - "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere - "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest - "Default | UpdateKey": "Nexus:433", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "No Crows": { - "ID": "cat.nocrows", - "Default | UpdateKey": "Nexus:1682" - }, - - "No Kids Ever": { - "ID": "Hangy.NoKidsEver", - "Default | UpdateKey": "Nexus:1464" - }, - - "No Debug Mode": { - "ID": "NoDebugMode", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - - "No Fence Decay": { - "ID": "cat.nofencedecay", - "Default | UpdateKey": "Nexus:1180" - }, - - "No More Pets": { - "ID": "Omegasis.NoMorePets", - "FormerIDs": "NoMorePets", // changed in 1.4 - "Default | UpdateKey": "Nexus:506" // added in 1.4.1 - }, - - "No Rumble Horse": { - "ID": "Xangria.NoRumbleHorse", - "Default | UpdateKey": "Nexus:1779" - }, - - "No Soil Decay": { - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "Default | UpdateKey": "Nexus:237", - "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location - }, - - "No Soil Decay Redux": { - "ID": "Platonymous.NoSoilDecayRedux", - "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 - }, - - "NPC Map Locations": { - "ID": "NPCMapLocationsMod", - "Default | UpdateKey": "Nexus:239", - "1.42~1.43 | Status": "AssumeBroken", - "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." - }, - - "NPC Speak": { - "FormerIDs": "{EntryDll: 'NpcEcho.dll'}", - "Default | UpdateKey": "Nexus:694", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Object Time Left": { - "ID": "spacechase0.ObjectTimeLeft", - "Default | UpdateKey": "Nexus:1315" - }, - - "OmniFarm": { - "ID": "PhthaloBlue.OmniFarm", - "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", - "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Out of Season Bonuses (Seasonal Items)": { - "ID": "midoriarmstrong.seasonalitems", - "Default | UpdateKey": "Nexus:1452" - }, - - "Part of the Community": { - "ID": "SB_PotC", - "Default | UpdateKey": "Nexus:923", - "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "PelicanFiber": { - "ID": "jwdred.PelicanFiber", - "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 - "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version - "Default | UpdateKey": "Nexus:631", - "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "PelicanTTS": { - "ID": "Platonymous.PelicanTTS", - "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 - "~1.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Persia the Mermaid - Standalone Custom NPC": { - "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "Default | UpdateKey": "Nexus:1419" - }, - - "Persistent Game Options": { - "ID": "Xangria.PersistentGameOptions", - "Default | UpdateKey": "Nexus:1778" - }, - - "Persival's BundleMod": { - "FormerIDs": "{EntryDll: 'BundleMod.dll'}", - "Default | UpdateKey": "Nexus:438", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - - "Plant on Grass": { - "ID": "Demiacle.PlantOnGrass", - "Default | UpdateKey": "Nexus:1026" - }, - - "PyTK - Platonymous Toolkit": { - "ID": "Platonymous.Toolkit", - "Default | UpdateKey": "Nexus:1726" - }, - - "Point-and-Plant": { - "ID": "jwdred.PointAndPlant", - "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 - "Default | UpdateKey": "Nexus:572", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Pony Weight Loss Program": { - "ID": "BadNetCode.PonyWeightLossProgram", - "Default | UpdateKey": "Nexus:1232" - }, - - "Portraiture": { - "ID": "Platonymous.Portraiture", - "Default | UpdateKey": "Nexus:999" // added in 1.3.1 - }, - - "Prairie King Made Easy": { - "ID": "Mucchan.PrairieKingMadeEasy", - "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 - "Default | UpdateKey": "Chucklefish:3594", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Purchasable Recipes": { - "ID": "Paracosm.PurchasableRecipes", - "Default | UpdateKey": "Nexus:1722" - }, - - "Quest Delay": { - "ID": "BadNetCode.QuestDelay", - "Default | UpdateKey": "Nexus:1239" - }, - - "Rain Randomizer": { - "FormerIDs": "{EntryDll: 'RainRandomizer.dll'}", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Recatch Legendary Fish": { - "ID": "cantorsdust.RecatchLegendaryFish", - "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 - "Default | UpdateKey": "Nexus:172" - }, - - "Regeneration": { - "ID": "HammurabiRegeneration", - "Default | UpdateKey": "Chucklefish:4584" - }, - - "Relationship Bar UI": { - "ID": "RelationshipBar", - "Default | UpdateKey": "Nexus:1009" - }, - - "RelationshipsEnhanced": { - "ID": "relationshipsenhanced", - "Default | UpdateKey": "Chucklefish:4435", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest - "Default | UpdateKey": "Nexus:751", - "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Rented Tools": { - "ID": "JarvieK.RentedTools", - "Default | UpdateKey": "Nexus:1307" - }, - - "Replanter": { - "ID": "jwdred.Replanter", - "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 - "Default | UpdateKey": "Nexus:589", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" }, - "Default | UpdateKey": "Chucklefish:4465" - }, - - "Reseed": { - "ID": "Roc.Reseed", - "Default | UpdateKey": "Nexus:887" - }, - - "Reusable Wallpapers and Floors (Wallpaper Retain)": { - "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "Default | UpdateKey": "Nexus:356", - "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ring of Fire": { - "ID": "Platonymous.RingOfFire", - "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 - }, - - "Rise and Shine": { - "ID": "Yoshify.RiseAndShine", - "FormerIDs": "{EntryDll: 'RiseAndShine.dll'}", // changed in 1.1.1-whisk-update - "Default | UpdateKey": "Nexus:3" - }, - - "Rope Bridge": { - "ID": "RopeBridge", - "Default | UpdateKey": "Nexus:824" - }, - - "Rotate Toolbar": { - "ID": "Pathoschild.RotateToolbar", - "Default | UpdateKey": "Nexus:1100" - }, - - "Rush Orders": { - "ID": "spacechase0.RushOrders", - "FormerIDs": "RushOrders", // changed in 1.1 - "Default | UpdateKey": "Nexus:605", - "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Save Anywhere": { - "ID": "Omegasis.SaveAnywhere", - "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl - "Default | UpdateKey": "Nexus:444", // added in 2.6.1 - "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Save Backup": { - "ID": "Omegasis.SaveBackup", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:435", // added in 1.3.1 - "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Scroll to Blank": { - "ID": "caraxian.scroll.to.blank", - "Default | UpdateKey": "Chucklefish:4405" - }, - - "Scythe Harvesting": { - "ID": "mmanlapat.ScytheHarvesting", - "FormerIDs": "ScytheHarvesting", // changed in 1.6 - "Default | UpdateKey": "Nexus:1106" - }, - - "SDV Twitch": { - "ID": "MTD.SDVTwitch", - "Default | UpdateKey": "Nexus:1760" - }, - - "Seasonal Immersion": { - "ID": "Entoarox.SeasonalImmersion", - "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) - "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Seed Bag": { - "ID": "Platonymous.SeedBag", - "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 - }, - - "Seed Catalogue": { - "ID": "spacechase0.SeedCatalogue", - "Default | UpdateKey": "Nexus:1640" - }, - - "Self Service": { - "ID": "JarvieK.SelfService", - "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated - "Default | UpdateKey": "Nexus:1304" - }, - - "Send Items": { - "ID": "Denifia.SendItems", - "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shed Notifications (BuildingsNotifications)": { - "ID": "TheCroak.BuildingsNotifications", - "Default | UpdateKey": "Nexus:620", - "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shenandoah Project": { - "ID": "Nishtra.ShenandoahProject", - "FormerIDs": "Shenandoah Project", // changed in 1.2 - "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest - "Default | UpdateKey": "Nexus:756", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ship Anywhere": { - "ID": "spacechase0.ShipAnywhere", - "Default | UpdateKey": "Nexus:1379" - }, - - "Shipment Tracker": { - "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "Default | UpdateKey": "Nexus:321", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shop Expander": { - "ID": "Entoarox.ShopExpander", - "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - "MapRemoteVersions": { "1.6.0b": "1.6.0" }, - "~1.6 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.6 by request (has its own update-check feature) - "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" }, - "Default | UpdateKey": "Chucklefish:4487", - "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shroom Spotter": { - "ID": "TehPers.ShroomSpotter", - "Default | UpdateKey": "Nexus:908" - }, - - "Simple Crop Label": { - "ID": "SimpleCropLabel", - "Default | UpdateKey": "Nexus:314" - }, - - "Simple Sound Manager": { - "ID": "Omegasis.SimpleSoundManager", - "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Simple Sprinklers": { - "ID": "tZed.SimpleSprinkler", - "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 - "Default | UpdateKey": "Nexus:76", - "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", - "MapLocalVersions": { "0.0": "1.4" }, - "Default | UpdateKey": "Nexus:366", - "~1.2.2 | Status": "AssumeBroken" // broke in SMAPI 1.9 (has multiple Mod instances) - }, - - "Skill Prestige": { - "ID": "alphablackwolf.skillPrestige", - "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 - "Default | UpdateKey": "Nexus:569", - "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:569", - "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Skip Intro": { - "ID": "Pathoschild.SkipIntro", - "FormerIDs": "SkipIntro", // changed in 1.4 - "Default | UpdateKey": "Nexus:533" - }, - - "Skull Cavern Elevator": { - "ID": "SkullCavernElevator", - "Default | UpdateKey": "Nexus:963" - }, - - "Skull Cave Saver": { - "ID": "cantorsdust.SkullCaveSaver", - "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 - "Default | UpdateKey": "Nexus:175" - }, - - "Sleepy Eye": { - "ID": "spacechase0.SleepyEye", - "Default | UpdateKey": "Nexus:1152" - }, - - "Slower Fence Decay": { - "ID": "Speeder.SlowerFenceDecay", - "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update - "Default | UpdateKey": "Nexus:252", - "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Smart Mod": { - "ID": "KuroBear.SmartMod", - "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Solar Eclipse Event": { - "ID": "KoihimeNakamura.SolarEclipseEvent", - "Default | UpdateKey": "Nexus:897", - "MapLocalVersions": { "1.3-20170917": "1.3" }, - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "SpaceCore": { - "ID": "spacechase0.SpaceCore", - "Default | UpdateKey": "Nexus:1348" - }, - - "Speedster": { - "ID": "Platonymous.Speedster", - "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 - }, - - "Sprinkler Range": { - "ID": "cat.sprinklerrange", - "Default | UpdateKey": "Nexus:1179" - }, - - "Sprinkles": { - "ID": "Platonymous.Sprinkles", - "Default | UpdateKey": "Chucklefish:4592", - "~1.1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Sprint and Dash": { - "ID": "SPDSprintAndDash", - "Default | UpdateKey": "Chucklefish:3531", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Sprint and Dash Redux": { - "ID": "littleraskol.SprintAndDashRedux", - "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 - "Default | UpdateKey": "Chucklefish:4201" - }, - - "Sprinting Mod": { - "FormerIDs": "{EntryDll: 'SprintingMod.dll'}", - "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest - "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", - "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "StackSplitX": { - "ID": "tstaples.StackSplitX", - "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 - "Default | UpdateKey": "Nexus:798", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "StaminaRegen": { - "FormerIDs": "{EntryDll: 'StaminaRegen.dll'}", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stardew Config Menu": { - "ID": "Juice805.StardewConfigMenu", - "Default | UpdateKey": "Nexus:1312" - }, - - "Stardew Content Compatibility Layer (SCCL)": { - "ID": "SCCL", - "Default | UpdateKey": "Nexus:889", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Stardew Editor Game Integration": { - "ID": "spacechase0.StardewEditor.GameIntegration", - "Default | UpdateKey": "Nexus:1298" - }, - - "Stardew Notification": { - "ID": "stardewnotification", - "Default | UpdateKey": "GitHub:monopandora/StardewNotification", - "~1.7 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stardew Symphony": { - "ID": "Omegasis.StardewSymphony", - "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:425", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "StarDustCore": { - "ID": "StarDustCore", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - }, - - "Starting Money": { - "ID": "mmanlapat.StartingMoney", - "FormerIDs": "StartingMoney", // changed in 1.1 - "Default | UpdateKey": "Nexus:1138" - }, - - "StashItemsToChest": { - "ID": "BlueMod_StashItemsToChest", - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stephan's Lots of Crops": { - "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated - "Default | UpdateKey": "Chucklefish:4314" - }, - - "Stone Bridge Over Pond (PondWithBridge)": { - "FormerIDs": "{EntryDll: 'PondWithBridge.dll'}", - "MapLocalVersions": { "0.0": "1.0" }, - "Default | UpdateKey": "Nexus:316", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stumps to Hardwood Stumps": { - "ID": "StumpsToHardwoodStumps", - "Default | UpdateKey": "Nexus:691" - }, - - "Super Greenhouse Warp Modifier": { - "ID": "SuperGreenhouse", - "Default | UpdateKey": "Chucklefish:4334", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Swim Almost Anywhere / Swim Suit": { - "ID": "Platonymous.SwimSuit", - "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 - }, - - "Tainted Cellar": { - "ID": "TaintedCellar", - "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}", // changed in 1.1 - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 - }, - - "Tapper Ready": { - "ID": "skunkkk.TapperReady", - "Default | UpdateKey": "Nexus:1219" - }, - - "Teh's Fishing Overhaul": { - "ID": "TehPers.FishingOverhaul", - "Default | UpdateKey": "Nexus:866" - }, - - "Teleporter": { - "ID": "Teleporter", - "Default | UpdateKey": "Chucklefish:4374", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "The Long Night": { - "ID": "Pathoschild.TheLongNight", - "Default | UpdateKey": "Nexus:1369" - }, - - "Three-heart Dance Partner": { - "ID": "ThreeHeartDancePartner", - "Default | UpdateKey": "Nexus:500", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "TimeFreeze": { - "ID": "Omegasis.TimeFreeze", - "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 - "Default | UpdateKey": "Nexus:973" // added in 1.2.1 - }, - - "Time Reminder": { - "ID": "KoihimeNakamura.TimeReminder", - "MapLocalVersions": { "1.0-20170314": "1.0.2" }, - "Default | UpdateKey": "Nexus:1000" - }, - - "TimeSpeed": { - "ID": "cantorsdust.TimeSpeed", - "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:169", - "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "To Do List": { - "ID": "eleanor.todolist", - "Default | UpdateKey": "Nexus:1630" - }, - - "Tool Charging": { - "ID": "mralbobo.ToolCharging", - "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" - }, - - "TractorMod": { - "ID": "Pathoschild.TractorMod", - "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Default | UpdateKey": "Nexus:1401" - }, - - "TrainerMod": { - "ID": "SMAPI.TrainerMod", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." - }, - - "Tree Transplant": { - "ID": "TreeTransplant", - "Default | UpdateKey": "Nexus:1342" - }, - - "UI Info Suite": { - "ID": "Cdaragorn.UiInfoSuite", - "Default | UpdateKey": "Nexus:1150" - }, - - "UiModSuite": { - "ID": "Demiacle.UiModSuite", - "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest - "Default | UpdateKey": "Nexus:1023", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Variable Grass": { - "ID": "dantheman999.VariableGrass", - "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" - }, - - "Vertical Toolbar": { - "ID": "SB_VerticalToolMenu", - "Default | UpdateKey": "Nexus:943" - }, - - "WakeUp": { - "FormerIDs": "{EntryDll: 'WakeUp.dll'}", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Wallpaper Fix": { - "FormerIDs": "{EntryDll: 'WallpaperFix.dll'}", - "Default | UpdateKey": "Chucklefish:4211", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "WarpAnimals": { - "ID": "Symen.WarpAnimals", - "Default | UpdateKey": "Nexus:1400" - }, - - "Weather Controller": { - "FormerIDs": "{EntryDll: 'WeatherController.dll'}", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "What Farm Cave / WhatAMush": { - "ID": "WhatAMush", - "Default | UpdateKey": "Nexus:1097" - }, - - "WHats Up": { - "ID": "wHatsUp", - "Default | UpdateKey": "Nexus:1082" - }, - - "Winter Grass": { - "ID": "cat.wintergrass", - "Default | UpdateKey": "Nexus:1601" - }, - - "Wonderful Farm Life": { - "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 - }, - - "XmlSerializerRetool": { - "FormerIDs": "{EntryDll: 'XmlSerializerRetool.dll'}", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "Xnb Loader": { - "ID": "Entoarox.XnbLoader", - "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "zDailyIncrease": { - "ID": "zdailyincrease", - "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest - "Default | UpdateKey": "Chucklefish:4247", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoom Out Extreme": { - "ID": "RockinMods.ZoomMod", - "FormerIDs": "ZoomMod", // changed circa 1.2.1 - "Default | UpdateKey": "Nexus:1326", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Better RNG": { - "ID": "Zoryn.BetterRNG", - "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Calendar Anywhere": { - "ID": "Zoryn.CalendarAnywhere", - "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Durable Fences": { - "ID": "Zoryn.DurableFences", - "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Health Bars": { - "ID": "Zoryn.HealthBars", - "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Fishing Mod": { - "ID": "Zoryn.FishingMod", - "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Junimo Deposit Anywhere": { - "ID": "Zoryn.JunimoDepositAnywhere", - "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Movement Mod": { - "ID": "Zoryn.MovementModifier", - "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Regen Mod": { - "ID": "Zoryn.RegenMod", - "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - } - } + "VerboseLogging": false } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c0d5386a..9dbca475 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -100,6 +100,7 @@ + @@ -262,6 +263,9 @@ Always + + PreserveNewest + diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json new file mode 100644 index 00000000..237c069d --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -0,0 +1,1836 @@ +{ + /** + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. + * + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). + * + * - ID: the mod's latest unique ID (if any). + * + * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching + * other fields if no ID was specified. This doesn't include the latest ID, if any. + * Format rules: + * 1. If the mod's ID changed over time, multiple variants can be separated by the '|' + * character. + * 2. Each variant can take one of two forms: + * - A simple string matching the mod's UniqueID value. + * - A JSON structure containing any of four manifest fields (ID, Name, Author, and + * EntryDll) to match. + * + * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. + * + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. + */ + "ModData": { + "AccessChestAnywhere": { + "ID": "AccessChestAnywhere", + "MapLocalVersions": { "1.1-1078": "1.1" }, + "Default | UpdateKey": "Nexus:257", + "~1.1 | Status": "AssumeBroken" + }, + + "AdjustArtisanPrices": { + "ID": "ThatNorthernMonkey.AdjustArtisanPrices", + "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update + "MapRemoteVersions": { "0.01": "0.0.1" }, + "Default | UpdateKey": "Chucklefish:3532", + "~0.0.1 | Status": "AssumeBroken" + }, + + "Adjust Monster": { + "ID": "mmanlapat.AdjustMonster", + "Default | UpdateKey": "Nexus:1161" + }, + + "Advanced Location Loader": { + "ID": "Entoarox.AdvancedLocationLoader", + "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) + "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Adventure Shop Inventory": { + "ID": "HammurabiAdventureShopInventory", + "Default | UpdateKey": "Chucklefish:4608" + }, + + "AgingMod": { + "ID": "skn.AgingMod", + "Default | UpdateKey": "Nexus:1129", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "All Crops All Seasons": { + "ID": "cantorsdust.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 + "Default | UpdateKey": "Nexus:170" + }, + + "All Professions": { + "ID": "cantorsdust.AllProfessions", + "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:174" + }, + + "Almighty Tool": { + "ID": "439", + "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 + "MapRemoteVersions": { "1.21": "1.2.1" }, + "Default | UpdateKey": "Nexus:439", + "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Animal Husbandry": { + "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 + "Default | UpdateKey": "Nexus:1538" + }, + + "Animal Mood Fix": { + "ID": "GPeters-AnimalMoodFix", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + + "Animal Sitter": { + "ID": "jwdred.AnimalSitter", + "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 + "Default | UpdateKey": "Nexus:581", + "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ashley Mod": { + "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "A Tapper's Dream": { + "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", + "Default | UpdateKey": "Nexus:260", + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Auto Animal Doors": { + "ID": "AaronTaggart.AutoAnimalDoors", + "Default | UpdateKey": "Nexus:1019" + }, + + "Auto-Eat": { + "ID": "Permamiss.AutoEat", + "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 + "Default | UpdateKey": "Nexus:643" + }, + + "AutoFish": { + "ID": "WhiteMind.AF", + "Default | UpdateKey": "Nexus:1895" + }, + + "AutoGate": { + "ID": "AutoGate", + "Default | UpdateKey": "Nexus:820" + }, + + "Automate": { + "ID": "Pathoschild.Automate", + "Default | UpdateKey": "Nexus:1063" + }, + + "Automated Doors": { + "ID": "azah.automated-doors", + "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 + "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 + }, + + "AutoSpeed": { + "ID": "Omegasis.AutoSpeed", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:443" // added in 1.4.1 + }, + + "Basic Sprinklers Improved": { + "ID": "lrsk_sdvm_bsi.0117171308", + "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:833" + }, + + "Better Hay": { + "ID": "cat.betterhay", + "Default | UpdateKey": "Nexus:1430" + }, + + "Better Quality More Seasons": { + "ID": "SB_BQMS", + "Default | UpdateKey": "Nexus:935" + }, + + "Better Quarry": { + "ID": "BetterQuarry", + "Default | UpdateKey": "Nexus:771" + }, + + "Better Ranching": { + "ID": "BetterRanching", + "Default | UpdateKey": "Nexus:859" + }, + + "Better Shipping Box": { + "ID": "Kithio:BetterShippingBox", + "MapLocalVersions": { "1.0.1": "1.0.2" }, + "Default | UpdateKey": "Chucklefish:4302", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "FormerIDs": "SPDSprinklersMod", // changed in 2.3 + "Default | UpdateKey": "Nexus:41", + "~2.3.1-pathoschild-update | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Billboard Anywhere": { + "ID": "Omegasis.BillboardAnywhere", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:492" // added in 1.4.1 + }, + + "Birthday Mail": { + "ID": "KathrynHazuka.BirthdayMail", + "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update + "Default | UpdateKey": "Nexus:276", + "~1.2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Breed Like Rabbits": { + "ID": "dycedarger.breedlikerabbits", + "Default | UpdateKey": "Nexus:948" + }, + + "Build Endurance": { + "ID": "Omegasis.BuildEndurance", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:445", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Build Health": { + "ID": "Omegasis.BuildHealth", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:446", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Buy Cooking Recipes": { + "ID": "Denifia.BuyRecipes", + "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Buy Back Collectables": { + "ID": "Omegasis.BuyBackCollectables", + "FormerIDs": "BuyBackCollectables", // changed in 1.4 + "Default | UpdateKey": "Nexus:507", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Carry Chest": { + "ID": "spacechase0.CarryChest", + "Default | UpdateKey": "Nexus:1333" + }, + + "Casks Anywhere": { + "ID": "CasksAnywhere", + "MapLocalVersions": { "1.1-alpha": "1.1" }, + "Default | UpdateKey": "Nexus:878" + }, + + "Categorize Chests": { + "ID": "CategorizeChests", + "Default | UpdateKey": "Nexus:1300" + }, + + "Chefs Closet": { + "ID": "Duder.ChefsCloset", + "MapLocalVersions": { "1.3-1": "1.3" }, + "Default | UpdateKey": "Nexus:1030" + }, + + "Chest Label System": { + "ID": "Speeder.ChestLabel", + "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update + "Default | UpdateKey": "Nexus:242", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "Chest Pooling": { + "ID": "mralbobo.ChestPooling", + "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 + "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Chests Anywhere": { + "ID": "Pathoschild.ChestsAnywhere", + "FormerIDs": "ChestsAnywhere", // changed in 1.9 + "Default | UpdateKey": "Nexus:518", + "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Choose Baby Gender": { + "FormerIDs": "{EntryDll: 'ChooseBabyGender.dll'}", + "Default | UpdateKey": "Nexus:590", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "CJB Automation": { + "ID": "CJBAutomation", + "Default | UpdateKey": "Nexus:211", + "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" + }, + + "CJB Cheats Menu": { + "ID": "CJBok.CheatsMenu", + "FormerIDs": "CJBCheatsMenu", // changed in 1.14 + "Default | UpdateKey": "Nexus:4", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "CJB Item Spawner": { + "ID": "CJBok.ItemSpawner", + "FormerIDs": "CJBItemSpawner", // changed in 1.7 + "Default | UpdateKey": "Nexus:93", + "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "CJB Show Item Sell Price": { + "ID": "CJBok.ShowItemSellPrice", + "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 + "Default | UpdateKey": "Nexus:5", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Clean Farm": { + "ID": "tstaples.CleanFarm", + "Default | UpdateKey": "Nexus:794" + }, + + "Climates of Ferngill": { + "ID": "KoihimeNakamura.ClimatesOfFerngill", + "Default | UpdateKey": "Nexus:604", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Coal Regen": { + "ID": "Blucifer.CoalRegen", + "Default | UpdateKey": "Nexus:1664" + }, + + "Cold Weather Haley": { + "ID": "LordXamon.ColdWeatherHaleyPRO", + "Default | UpdateKey": "Nexus:1169", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Colored Chests": { + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + + "Combat with Farm Implements": { + "ID": "SPDFarmingImplementsInCombat", + "Default | UpdateKey": "Nexus:313", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Community Bundle Item Tooltip": { + "ID": "musbah.bundleTooltip", + "Default | UpdateKey": "Nexus:1329" + }, + + "Concentration on Farming": { + "ID": "punyo.ConcentrationOnFarming", + "Default | UpdateKey": "Nexus:1445" + }, + + "Configurable Machines": { + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "MapLocalVersions": { "1.2-beta": "1.2" }, + "Default | UpdateKey": "Nexus:280" + }, + + "Configurable Shipping Dates": { + "ID": "ConfigurableShippingDates", + "Default | UpdateKey": "Nexus:675", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Cooking Skill": { + "ID": "spacechase0.CookingSkill", + "FormerIDs": "CookingSkill", // changed in 1.0.4–6 + "Default | UpdateKey": "Nexus:522", + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "CrabNet": { + "ID": "jwdred.CrabNet", + "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 + "Default | UpdateKey": "Nexus:584", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Crafting Counter": { + "ID": "lolpcgaming.CraftingCounter", + "Default | UpdateKey": "Nexus:1585" + }, + + "Current Location": { + "ID": "CurrentLocation102120161203", + "Default | UpdateKey": "Nexus:638" + }, + + "Custom Asset Modifier": { + "ID": "Omegasis.CustomAssetModifier", + "Default | UpdateKey": "1836" + }, + + "Custom Critters": { + "ID": "spacechase0.CustomCritters", + "Default | UpdateKey": "Nexus:1255" + }, + + "Custom Crops": { + "ID": "spacechase0.CustomCrops", + "Default | UpdateKey": "Nexus:1592" + }, + + "Custom Element Handler": { + "ID": "Platonymous.CustomElementHandler", + "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 + }, + + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991" // added in 0.6.1 + }, + + "Custom Farming Automate Bridge": { + "ID": "Platonymous.CFAutomate", + "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate + "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" + }, + + "Custom Farm Types": { + "ID": "spacechase0.CustomFarmTypes", + "Default | UpdateKey": "Nexus:1140" + }, + + "Custom Furniture": { + "ID": "Platonymous.CustomFurniture", + "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 + }, + + "Customize Exterior": { + "ID": "spacechase0.CustomizeExterior", + "FormerIDs": "CustomizeExterior", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:1099", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Customizable Cart Redux": { + "ID": "KoihimeNakamura.CCR", + "MapLocalVersions": { "1.1-20170917": "1.1" }, + "Default | UpdateKey": "Nexus:1402" + }, + + "Customizable Traveling Cart Days": { + "ID": "TravelingCartYyeahdude", + "Default | UpdateKey": "Nexus:567", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Custom Linens": { + "ID": "Mevima.CustomLinens", + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1027" + }, + + "Custom NPC": { + "ID": "Platonymous.CustomNPC", + "Default | UpdateKey": "Nexus:1607" + }, + + "Custom Shops Redux": { + "ID": "Omegasis.CustomShopReduxGui", + "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 + }, + + "Custom TV": { + "ID": "Platonymous.CustomTV", + "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 + }, + + "Daily Luck Message": { + "ID": "Schematix.DailyLuckMessage", + "Default | UpdateKey": "Nexus:1327" + }, + + "Daily News": { + "ID": "bashNinja.DailyNews", + "Default | UpdateKey": "Nexus:1141", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Daily Quest Anywhere": { + "ID": "Omegasis.DailyQuestAnywhere", + "FormerIDs": "DailyQuest", // changed in 1.4 + "Default | UpdateKey": "Nexus:513" // added in 1.4.1 + }, + + "Debug Mode": { + "ID": "Pathoschild.DebugMode", + "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 + "Default | UpdateKey": "Nexus:679" + }, + + "Did You Water Your Crops?": { + "ID": "Nishtra.DidYouWaterYourCrops", + "Default | UpdateKey": "Nexus:1583" + }, + + "Dynamic Checklist": { + "ID": "gunnargolf.DynamicChecklist", + "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Dynamic Horses": { + "ID": "Bpendragon-DynamicHorses", + "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:874" + }, + + "Dynamic Machines": { + "ID": "DynamicMachines", + "MapLocalVersions": { "1.1": "1.1.1" }, + "Default | UpdateKey": "Nexus:374", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Dynamic NPC Sprites": { + "ID": "BashNinja.DynamicNPCSprites", + "Default | UpdateKey": "Nexus:1183" + }, + + "Easier Farming": { + "ID": "cautiouswafffle.EasierFarming", + "Default | UpdateKey": "Nexus:1426" + }, + + "Empty Hands": { + "ID": "QuicksilverFox.EmptyHands", + "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Enemy Health Bars": { + "ID": "Speeder.HealthBars", + "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update + "Default | UpdateKey": "Nexus:193", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Entoarox Framework": { + "ID": "Entoarox.EntoaroxFramework", + "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? + "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) + "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) + }, + + "Expanded Fridge": { + "ID": "Uwazouri.ExpandedFridge", + "Default | UpdateKey": "Nexus:1191" + }, + + "Experience Bars": { + "ID": "spacechase0.ExperienceBars", + "FormerIDs": "ExperienceBars", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:509" + }, + + "Extended Bus System": { + "ID": "ExtendedBusSystem", + "Default | UpdateKey": "Chucklefish:4373" + }, + + "Extended Fridge": { + "ID": "Crystalmir.ExtendedFridge", + "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 + "Default | UpdateKey": "Nexus:485", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Extended Greenhouse": { + "ID": "ExtendedGreenhouse", + "Default | UpdateKey": "Chucklefish:4303", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Extended Minecart": { + "ID": "Entoarox.ExtendedMinecart", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 + "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) + }, + + "Extended Reach": { + "ID": "spacechase0.ExtendedReach", + "Default | UpdateKey": "Nexus:1493" + }, + + "Fall 28 Snow Day": { + "ID": "Omegasis.Fall28SnowDay", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:486", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Automation: Barn Door Automation": { + "FormerIDs": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Automation: Item Collector": { + "FormerIDs": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Farm Automation Unofficial: Item Collector": { + "ID": "Maddy99.FarmAutomation.ItemCollector", + "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Expansion": { + "ID": "Advize.FarmExpansion", + "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 + "Default | UpdateKey": "Nexus:130", + "~2.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Resource Generator": { + "FormerIDs": "{EntryDll: 'FarmResourceGenerator.dll'}", + "Default | UpdateKey": "Nexus:647", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Fast Animations": { + "ID": "Pathoschild.FastAnimations", + "Default | UpdateKey": "Nexus:1089" + }, + + "Faster Grass": { + "ID": "IceGladiador.FasterGrass", + "Default | UpdateKey": "Nexus:1772" + }, + + "Faster Paths": { + "ID": "Entoarox.FasterPaths", + "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander + "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) + }, + + "Faster Run": { + "ID": "KathrynHazuka.FasterRun", + "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update + "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Fishing Adjust": { + "ID": "shuaiz.FishingAdjustMod", + "Default | UpdateKey": "Nexus:1350" + }, + + "Fishing Tuner Redux": { + "ID": "HammurabiFishingTunerRedux", + "Default | UpdateKey": "Chucklefish:4578" + }, + + "Fixed Secret Woods Debris": { + "ID": "f4iTh.WoodsDebrisFix", + "Default | UpdateKey": "Nexus:1941" + }, + + "FlorenceMod": { + "FormerIDs": "{EntryDll: 'FlorenceMod.dll'}", + "MapLocalVersions": { "1.0.1": "1.1" }, + "Default | UpdateKey": "Nexus:591", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Flower Color Picker": { + "ID": "spacechase0.FlowerColorPicker", + "Default | UpdateKey": "Nexus:1229" + }, + + "Forage at the Farm": { + "ID": "Nishtra.ForageAtTheFarm", + "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 + "Default | UpdateKey": "Nexus:673", + "~1.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Furniture Anywhere": { + "ID": "Entoarox.FurnitureAnywhere", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart + "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) + }, + + "Game Reminder": { + "ID": "mmanlapat.GameReminder", + "Default | UpdateKey": "Nexus:1153" + }, + + "Gate Opener": { + "ID": "mralbobo.GateOpener", + "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 + "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "GenericShopExtender": { + "ID": "GenericShopExtender", + "Default | UpdateKey": "Nexus:814", // added in 0.1.3 + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Geode Info Menu": { + "ID": "cat.geodeinfomenu", + "Default | UpdateKey": "Nexus:1448" + }, + + "Get Dressed": { + "ID": "Advize.GetDressed", + "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 + "Default | UpdateKey": "Nexus:331", + "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Giant Crop Ring": { + "ID": "cat.giantcropring", + "Default | UpdateKey": "Nexus:1182" + }, + + "Gift Taste Helper": { + "ID": "tstaples.GiftTasteHelper", + "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 + "Default | UpdateKey": "Nexus:229", + "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Grandfather's Gift": { + "ID": "ShadowDragon.GrandfathersGift", + "Default | UpdateKey": "Nexus:985" + }, + + "Happy Animals": { + "ID": "HappyAnimals", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Omegasis)": { + "ID": "Omegasis.HappyBirthday", + "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork + "Default | UpdateKey": "Nexus:520", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Oxyligen fork)": { + "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork + "Default | UpdateKey": "Nexus:1064" // missing key reported: https://www.nexusmods.com/stardewvalley/mods/1064?tab=bugs + }, + + "Hardcore Mines": { + "ID": "kibbe.hardcore_mines", + "Default | UpdateKey": "Nexus:1674" + }, + + "Harp of Yoba Redux": { + "ID": "Platonymous.HarpOfYobaRedux", + "Default | UpdateKey": "Nexus:914" // added in 2.0.3 + }, + + "Harvest Moon Witch Princess": { + "ID": "Sasara.WitchPrincess", + "Default | UpdateKey": "Nexus:1157" + }, + + "Harvest With Scythe": { + "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", + "Default | UpdateKey": "Nexus:236", + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Horse Whistle (icepuente)": { + "ID": "icepuente.HorseWhistle", + "Default | UpdateKey": "Nexus:1131" + }, + + "Hunger (Yyeadude)": { + "ID": "HungerYyeadude", + "Default | UpdateKey": "Nexus:613" + }, + + "Hunger for Food (Tigerle)": { + "ID": "HungerForFoodByTigerle", + "Default | UpdateKey": "Nexus:810", + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Hunger Mod (skn)": { + "ID": "skn.HungerMod", + "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1127" + }, + + "Idle Pause": { + "ID": "Veleek.IdlePause", + "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1092" + }, + + "Improved Quality of Life": { + "ID": "Demiacle.ImprovedQualityOfLife", + "Default | UpdateKey": "Nexus:1025", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Instant Geode": { + "ID": "InstantGeode", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Instant Grow Trees": { + "ID": "cantorsdust.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:173" + }, + + "Interaction Helper": { + "ID": "HammurabiInteractionHelper", + "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Item Auto Stacker": { + "ID": "cat.autostacker", + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1184" + }, + + "Jiggly Junimo Bundles": { + "ID": "Greger.JigglyJunimoBundles", + "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update + "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update + }, + + "Json Assets": { + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720" + }, + + "Junimo Farm": { + "ID": "Platonymous.JunimoFarm", + "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:984" // added in 1.1.3 + }, + + "Less Strict Over-Exertion (AntiExhaustion)": { + "ID": "BALANCEMOD_AntiExhaustion", + "MapLocalVersions": { "0.0": "1.1" }, + "Default | UpdateKey": "Nexus:637", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Level Extender": { + "ID": "Devin Lematty.Level Extender", + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1471" + }, + + "Level Up Notifications": { + "ID": "Level Up Notifications", + "MapRemoteVersions": { "0.0.1a": "0.0.1" }, + "Default | UpdateKey": "Nexus:855" + }, + + "Location and Music Logging": { + "ID": "Brandy Lover.LMlog", + "Default | UpdateKey": "Nexus:1366" + }, + + "Longevity": { + "ID": "RTGOAT.Longevity", + "MapRemoteVersions": { "1.6.8h": "1.6.8" }, + "Default | UpdateKey": "Nexus:649" + }, + + "Lookup Anything": { + "ID": "Pathoschild.LookupAnything", + "FormerIDs": "LookupAnything", // changed in 1.10.1 + "Default | UpdateKey": "Nexus:541", + "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Love Bubbles": { + "ID": "LoveBubbles", + "Default | UpdateKey": "Nexus:1318" + }, + + "Loved Labels": { + "ID": "Advize.LovedLabels", + "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 + "Default | UpdateKey": "Nexus:279", + "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Luck Skill": { + "ID": "spacechase0.LuckSkill", + "FormerIDs": "LuckSkill", // changed in 0.1.4 + "Default | UpdateKey": "Nexus:521", + "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mail Framework": { + "ID": "DIGUS.MailFrameworkMod", + "Default | UpdateKey": "Nexus:1536" + }, + + "MailOrderPigs": { + "ID": "jwdred.MailOrderPigs", + "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:632", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Makeshift Multiplayer": { + "ID": "spacechase0.StardewValleyMP", + "FormerIDs": "StardewValleyMP", // changed in 0.3 + "Default | UpdateKey": "Nexus:501", + "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Map Image Exporter": { + "ID": "spacechase0.MapImageExporter", + "FormerIDs": "MapImageExporter", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:1073" + }, + + "Message Box [API]? (ChatMod)": { + "ID": "Kithio:ChatMod", + "Default | UpdateKey": "Chucklefish:4296", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mining at the Farm": { + "ID": "Nishtra.MiningAtTheFarm", + "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 + "Default | UpdateKey": "Nexus:674" + }, + + "Mining With Explosives": { + "ID": "Nishtra.MiningWithExplosives", + "FormerIDs": "MiningWithExplosives", // changed in 1.1 + "Default | UpdateKey": "Nexus:770" + }, + + "Modder Serialization Utility": { + "ID": "SerializerUtils-0-1", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Monster Level Tip": { + "ID": "WhiteMind.MonsterLT", + "Default | UpdateKey": "Nexus:1896" + }, + + "More Animals": { + "ID": "Entoarox.MoreAnimals", + "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 + "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) + "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility + }, + + "More Artifact Spots": { + "ID": "451", + "Default | UpdateKey": "Nexus:451", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Map Layers": { + "ID": "Platonymous.MoreMapLayers", + "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 + }, + + "More Rain": { + "ID": "Omegasis.MoreRain", + "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:441", // added in 1.5.1 + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Weapons": { + "ID": "Joco80.MoreWeapons", + "Default | UpdateKey": "Nexus:1168" + }, + + "Move Faster": { + "ID": "shuaiz.MoveFasterMod", + "Default | UpdateKey": "Nexus:1351" + }, + + "Multiple Sprites and Portraits On Rotation (File Loading)": { + "ID": "FileLoading", + "MapLocalVersions": { "1.1": "1.12" }, + "Default | UpdateKey": "Nexus:1094", + "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Museum Rearranger": { + "ID": "Omegasis.MuseumRearranger", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:428", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mushroom Level Tip": { + "ID": "WhiteMind.MLT", + "Default | UpdateKey": "Nexus:1894" + }, + + "New Machines": { + "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", + "Default | UpdateKey": "Chucklefish:3683", + "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Night Owl": { + "ID": "Omegasis.NightOwl", + "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere + "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest + "Default | UpdateKey": "Nexus:433", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "No Crows": { + "ID": "cat.nocrows", + "Default | UpdateKey": "Nexus:1682" + }, + + "No Kids Ever": { + "ID": "Hangy.NoKidsEver", + "Default | UpdateKey": "Nexus:1464" + }, + + "No Debug Mode": { + "ID": "NoDebugMode", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, + + "No Fence Decay": { + "ID": "cat.nofencedecay", + "Default | UpdateKey": "Nexus:1180" + }, + + "No More Pets": { + "ID": "Omegasis.NoMorePets", + "FormerIDs": "NoMorePets", // changed in 1.4 + "Default | UpdateKey": "Nexus:506" // added in 1.4.1 + }, + + "No Rumble Horse": { + "ID": "Xangria.NoRumbleHorse", + "Default | UpdateKey": "Nexus:1779" + }, + + "No Soil Decay": { + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "Default | UpdateKey": "Nexus:237", + "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location + }, + + "No Soil Decay Redux": { + "ID": "Platonymous.NoSoilDecayRedux", + "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 + }, + + "NPC Map Locations": { + "ID": "NPCMapLocationsMod", + "Default | UpdateKey": "Nexus:239", + "1.42~1.43 | Status": "AssumeBroken", + "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." + }, + + "NPC Speak": { + "FormerIDs": "{EntryDll: 'NpcEcho.dll'}", + "Default | UpdateKey": "Nexus:694", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Object Time Left": { + "ID": "spacechase0.ObjectTimeLeft", + "Default | UpdateKey": "Nexus:1315" + }, + + "OmniFarm": { + "ID": "PhthaloBlue.OmniFarm", + "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", + "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Out of Season Bonuses (Seasonal Items)": { + "ID": "midoriarmstrong.seasonalitems", + "Default | UpdateKey": "Nexus:1452" + }, + + "Part of the Community": { + "ID": "SB_PotC", + "Default | UpdateKey": "Nexus:923", + "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "PelicanFiber": { + "ID": "jwdred.PelicanFiber", + "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 + "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version + "Default | UpdateKey": "Nexus:631", + "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "PelicanTTS": { + "ID": "Platonymous.PelicanTTS", + "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 + "~1.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Persia the Mermaid - Standalone Custom NPC": { + "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", + "Default | UpdateKey": "Nexus:1419" + }, + + "Persistent Game Options": { + "ID": "Xangria.PersistentGameOptions", + "Default | UpdateKey": "Nexus:1778" + }, + + "Persival's BundleMod": { + "FormerIDs": "{EntryDll: 'BundleMod.dll'}", + "Default | UpdateKey": "Nexus:438", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "Plant on Grass": { + "ID": "Demiacle.PlantOnGrass", + "Default | UpdateKey": "Nexus:1026" + }, + + "PyTK - Platonymous Toolkit": { + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" + }, + + "Point-and-Plant": { + "ID": "jwdred.PointAndPlant", + "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:572", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Pony Weight Loss Program": { + "ID": "BadNetCode.PonyWeightLossProgram", + "Default | UpdateKey": "Nexus:1232" + }, + + "Portraiture": { + "ID": "Platonymous.Portraiture", + "Default | UpdateKey": "Nexus:999" // added in 1.3.1 + }, + + "Prairie King Made Easy": { + "ID": "Mucchan.PrairieKingMadeEasy", + "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 + "Default | UpdateKey": "Chucklefish:3594", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Purchasable Recipes": { + "ID": "Paracosm.PurchasableRecipes", + "Default | UpdateKey": "Nexus:1722" + }, + + "Quest Delay": { + "ID": "BadNetCode.QuestDelay", + "Default | UpdateKey": "Nexus:1239" + }, + + "Rain Randomizer": { + "FormerIDs": "{EntryDll: 'RainRandomizer.dll'}", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Recatch Legendary Fish": { + "ID": "cantorsdust.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 + "Default | UpdateKey": "Nexus:172" + }, + + "Regeneration": { + "ID": "HammurabiRegeneration", + "Default | UpdateKey": "Chucklefish:4584" + }, + + "Relationship Bar UI": { + "ID": "RelationshipBar", + "Default | UpdateKey": "Nexus:1009" + }, + + "RelationshipsEnhanced": { + "ID": "relationshipsenhanced", + "Default | UpdateKey": "Chucklefish:4435", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Relationship Status": { + "ID": "relationshipstatus", + "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest + "Default | UpdateKey": "Nexus:751", + "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Rented Tools": { + "ID": "JarvieK.RentedTools", + "Default | UpdateKey": "Nexus:1307" + }, + + "Replanter": { + "ID": "jwdred.Replanter", + "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 + "Default | UpdateKey": "Nexus:589", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "ReRegeneration": { + "ID": "lrsk_sdvm_rerg.0925160827", + "MapLocalVersions": { "1.1.2-release": "1.1.2" }, + "Default | UpdateKey": "Chucklefish:4465" + }, + + "Reseed": { + "ID": "Roc.Reseed", + "Default | UpdateKey": "Nexus:887" + }, + + "Reusable Wallpapers and Floors (Wallpaper Retain)": { + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", + "Default | UpdateKey": "Nexus:356", + "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ring of Fire": { + "ID": "Platonymous.RingOfFire", + "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 + }, + + "Rise and Shine": { + "ID": "Yoshify.RiseAndShine", + "FormerIDs": "{EntryDll: 'RiseAndShine.dll'}", // changed in 1.1.1-whisk-update + "Default | UpdateKey": "Nexus:3" + }, + + "Rope Bridge": { + "ID": "RopeBridge", + "Default | UpdateKey": "Nexus:824" + }, + + "Rotate Toolbar": { + "ID": "Pathoschild.RotateToolbar", + "Default | UpdateKey": "Nexus:1100" + }, + + "Rush Orders": { + "ID": "spacechase0.RushOrders", + "FormerIDs": "RushOrders", // changed in 1.1 + "Default | UpdateKey": "Nexus:605", + "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Save Anywhere": { + "ID": "Omegasis.SaveAnywhere", + "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl + "Default | UpdateKey": "Nexus:444", // added in 2.6.1 + "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Save Backup": { + "ID": "Omegasis.SaveBackup", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:435", // added in 1.3.1 + "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Scroll to Blank": { + "ID": "caraxian.scroll.to.blank", + "Default | UpdateKey": "Chucklefish:4405" + }, + + "Scythe Harvesting": { + "ID": "mmanlapat.ScytheHarvesting", + "FormerIDs": "ScytheHarvesting", // changed in 1.6 + "Default | UpdateKey": "Nexus:1106" + }, + + "SDV Twitch": { + "ID": "MTD.SDVTwitch", + "Default | UpdateKey": "Nexus:1760" + }, + + "Seasonal Immersion": { + "ID": "Entoarox.SeasonalImmersion", + "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) + "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Seed Bag": { + "ID": "Platonymous.SeedBag", + "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 + }, + + "Seed Catalogue": { + "ID": "spacechase0.SeedCatalogue", + "Default | UpdateKey": "Nexus:1640" + }, + + "Self Service": { + "ID": "JarvieK.SelfService", + "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated + "Default | UpdateKey": "Nexus:1304" + }, + + "Send Items": { + "ID": "Denifia.SendItems", + "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shed Notifications (BuildingsNotifications)": { + "ID": "TheCroak.BuildingsNotifications", + "Default | UpdateKey": "Nexus:620", + "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shenandoah Project": { + "ID": "Nishtra.ShenandoahProject", + "FormerIDs": "Shenandoah Project", // changed in 1.2 + "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest + "Default | UpdateKey": "Nexus:756", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ship Anywhere": { + "ID": "spacechase0.ShipAnywhere", + "Default | UpdateKey": "Nexus:1379" + }, + + "Shipment Tracker": { + "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", + "Default | UpdateKey": "Nexus:321", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shop Expander": { + "ID": "Entoarox.ShopExpander", + "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + "MapRemoteVersions": { "1.6.0b": "1.6.0" }, + "~1.6 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.6 by request (has its own update-check feature) + "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Showcase Mod": { + "ID": "Igorious.Showcase", + "MapLocalVersions": { "0.9-500": "0.9" }, + "Default | UpdateKey": "Chucklefish:4487", + "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shroom Spotter": { + "ID": "TehPers.ShroomSpotter", + "Default | UpdateKey": "Nexus:908" + }, + + "Simple Crop Label": { + "ID": "SimpleCropLabel", + "Default | UpdateKey": "Nexus:314" + }, + + "Simple Sound Manager": { + "ID": "Omegasis.SimpleSoundManager", + "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Simple Sprinklers": { + "ID": "tZed.SimpleSprinkler", + "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 + "Default | UpdateKey": "Nexus:76", + "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Siv's Marriage Mod": { + "ID": "6266959802", + "MapLocalVersions": { "0.0": "1.4" }, + "Default | UpdateKey": "Nexus:366", + "~1.2.2 | Status": "AssumeBroken" // broke in SMAPI 1.9 (has multiple Mod instances) + }, + + "Skill Prestige": { + "ID": "alphablackwolf.skillPrestige", + "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 + "Default | UpdateKey": "Nexus:569", + "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Skill Prestige: Cooking Adapter": { + "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", + "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 + "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:569", + "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "FormerIDs": "SkipIntro", // changed in 1.4 + "Default | UpdateKey": "Nexus:533" + }, + + "Skull Cavern Elevator": { + "ID": "SkullCavernElevator", + "Default | UpdateKey": "Nexus:963" + }, + + "Skull Cave Saver": { + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 + "Default | UpdateKey": "Nexus:175" + }, + + "Sleepy Eye": { + "ID": "spacechase0.SleepyEye", + "Default | UpdateKey": "Nexus:1152" + }, + + "Slower Fence Decay": { + "ID": "Speeder.SlowerFenceDecay", + "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update + "Default | UpdateKey": "Nexus:252", + "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Smart Mod": { + "ID": "KuroBear.SmartMod", + "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Solar Eclipse Event": { + "ID": "KoihimeNakamura.SolarEclipseEvent", + "Default | UpdateKey": "Nexus:897", + "MapLocalVersions": { "1.3-20170917": "1.3" }, + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "SpaceCore": { + "ID": "spacechase0.SpaceCore", + "Default | UpdateKey": "Nexus:1348" + }, + + "Speedster": { + "ID": "Platonymous.Speedster", + "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 + }, + + "Sprinkler Range": { + "ID": "cat.sprinklerrange", + "Default | UpdateKey": "Nexus:1179" + }, + + "Sprinkles": { + "ID": "Platonymous.Sprinkles", + "Default | UpdateKey": "Chucklefish:4592", + "~1.1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Sprint and Dash": { + "ID": "SPDSprintAndDash", + "Default | UpdateKey": "Chucklefish:3531", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Sprint and Dash Redux": { + "ID": "littleraskol.SprintAndDashRedux", + "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 + "Default | UpdateKey": "Chucklefish:4201" + }, + + "Sprinting Mod": { + "FormerIDs": "{EntryDll: 'SprintingMod.dll'}", + "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest + "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", + "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "StackSplitX": { + "ID": "tstaples.StackSplitX", + "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 + "Default | UpdateKey": "Nexus:798", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "StaminaRegen": { + "FormerIDs": "{EntryDll: 'StaminaRegen.dll'}", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stardew Config Menu": { + "ID": "Juice805.StardewConfigMenu", + "Default | UpdateKey": "Nexus:1312" + }, + + "Stardew Content Compatibility Layer (SCCL)": { + "ID": "SCCL", + "Default | UpdateKey": "Nexus:889", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Stardew Editor Game Integration": { + "ID": "spacechase0.StardewEditor.GameIntegration", + "Default | UpdateKey": "Nexus:1298" + }, + + "Stardew Notification": { + "ID": "stardewnotification", + "Default | UpdateKey": "GitHub:monopandora/StardewNotification", + "~1.7 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stardew Symphony": { + "ID": "Omegasis.StardewSymphony", + "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:425", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "StarDustCore": { + "ID": "StarDustCore", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, + + "Starting Money": { + "ID": "mmanlapat.StartingMoney", + "FormerIDs": "StartingMoney", // changed in 1.1 + "Default | UpdateKey": "Nexus:1138" + }, + + "StashItemsToChest": { + "ID": "BlueMod_StashItemsToChest", + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stephan's Lots of Crops": { + "ID": "stephansstardewcrops", + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "Default | UpdateKey": "Chucklefish:4314" + }, + + "Stone Bridge Over Pond (PondWithBridge)": { + "FormerIDs": "{EntryDll: 'PondWithBridge.dll'}", + "MapLocalVersions": { "0.0": "1.0" }, + "Default | UpdateKey": "Nexus:316", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stumps to Hardwood Stumps": { + "ID": "StumpsToHardwoodStumps", + "Default | UpdateKey": "Nexus:691" + }, + + "Super Greenhouse Warp Modifier": { + "ID": "SuperGreenhouse", + "Default | UpdateKey": "Chucklefish:4334", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Swim Almost Anywhere / Swim Suit": { + "ID": "Platonymous.SwimSuit", + "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 + }, + + "Tainted Cellar": { + "ID": "TaintedCellar", + "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}", // changed in 1.1 + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 + }, + + "Tapper Ready": { + "ID": "skunkkk.TapperReady", + "Default | UpdateKey": "Nexus:1219" + }, + + "Teh's Fishing Overhaul": { + "ID": "TehPers.FishingOverhaul", + "Default | UpdateKey": "Nexus:866" + }, + + "Teleporter": { + "ID": "Teleporter", + "Default | UpdateKey": "Chucklefish:4374", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "The Long Night": { + "ID": "Pathoschild.TheLongNight", + "Default | UpdateKey": "Nexus:1369" + }, + + "Three-heart Dance Partner": { + "ID": "ThreeHeartDancePartner", + "Default | UpdateKey": "Nexus:500", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "TimeFreeze": { + "ID": "Omegasis.TimeFreeze", + "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 + "Default | UpdateKey": "Nexus:973" // added in 1.2.1 + }, + + "Time Reminder": { + "ID": "KoihimeNakamura.TimeReminder", + "MapLocalVersions": { "1.0-20170314": "1.0.2" }, + "Default | UpdateKey": "Nexus:1000" + }, + + "TimeSpeed": { + "ID": "cantorsdust.TimeSpeed", + "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:169", + "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "To Do List": { + "ID": "eleanor.todolist", + "Default | UpdateKey": "Nexus:1630" + }, + + "Tool Charging": { + "ID": "mralbobo.ToolCharging", + "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" + }, + + "TractorMod": { + "ID": "Pathoschild.TractorMod", + "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "Default | UpdateKey": "Nexus:1401" + }, + + "TrainerMod": { + "ID": "SMAPI.TrainerMod", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." + }, + + "Tree Transplant": { + "ID": "TreeTransplant", + "Default | UpdateKey": "Nexus:1342" + }, + + "UI Info Suite": { + "ID": "Cdaragorn.UiInfoSuite", + "Default | UpdateKey": "Nexus:1150" + }, + + "UiModSuite": { + "ID": "Demiacle.UiModSuite", + "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest + "Default | UpdateKey": "Nexus:1023", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Variable Grass": { + "ID": "dantheman999.VariableGrass", + "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" + }, + + "Vertical Toolbar": { + "ID": "SB_VerticalToolMenu", + "Default | UpdateKey": "Nexus:943" + }, + + "WakeUp": { + "FormerIDs": "{EntryDll: 'WakeUp.dll'}", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Wallpaper Fix": { + "FormerIDs": "{EntryDll: 'WallpaperFix.dll'}", + "Default | UpdateKey": "Chucklefish:4211", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "WarpAnimals": { + "ID": "Symen.WarpAnimals", + "Default | UpdateKey": "Nexus:1400" + }, + + "Weather Controller": { + "FormerIDs": "{EntryDll: 'WeatherController.dll'}", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "What Farm Cave / WhatAMush": { + "ID": "WhatAMush", + "Default | UpdateKey": "Nexus:1097" + }, + + "WHats Up": { + "ID": "wHatsUp", + "Default | UpdateKey": "Nexus:1082" + }, + + "Winter Grass": { + "ID": "cat.wintergrass", + "Default | UpdateKey": "Nexus:1601" + }, + + "Wonderful Farm Life": { + "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 + }, + + "XmlSerializerRetool": { + "FormerIDs": "{EntryDll: 'XmlSerializerRetool.dll'}", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Xnb Loader": { + "ID": "Entoarox.XnbLoader", + "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "zDailyIncrease": { + "ID": "zdailyincrease", + "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest + "Default | UpdateKey": "Chucklefish:4247", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoom Out Extreme": { + "ID": "RockinMods.ZoomMod", + "FormerIDs": "ZoomMod", // changed circa 1.2.1 + "Default | UpdateKey": "Nexus:1326", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Better RNG": { + "ID": "Zoryn.BetterRNG", + "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Calendar Anywhere": { + "ID": "Zoryn.CalendarAnywhere", + "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Durable Fences": { + "ID": "Zoryn.DurableFences", + "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Health Bars": { + "ID": "Zoryn.HealthBars", + "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Fishing Mod": { + "ID": "Zoryn.FishingMod", + "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Junimo Deposit Anywhere": { + "ID": "Zoryn.JunimoDepositAnywhere", + "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Movement Mod": { + "ID": "Zoryn.MovementModifier", + "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Regen Mod": { + "ID": "Zoryn.RegenMod", + "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + } + } +} -- cgit From 504733dec7d629335b83841af38cd5da91d5231f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 23:00:49 -0400 Subject: fix console color scheme for PowerShell, and make it configurable --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/MonitorColorScheme.cs | 15 ++++++ src/SMAPI/Framework/Models/SConfig.cs | 3 ++ src/SMAPI/Framework/Monitor.cs | 65 ++++++++++++++---------- src/SMAPI/Program.cs | 4 +- src/SMAPI/StardewModdingAPI.config.json | 10 +++- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 src/SMAPI/Framework/Models/MonitorColorScheme.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index f0202ee1..817fcd47 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. + * Fixed console color scheme for PowerShell and added override option to `StardewModdingAPI.config.json`. * For modders: * Added code analysis to mod build config package to flag common issues as warnings. diff --git a/src/SMAPI/Framework/Models/MonitorColorScheme.cs b/src/SMAPI/Framework/Models/MonitorColorScheme.cs new file mode 100644 index 00000000..d8289d08 --- /dev/null +++ b/src/SMAPI/Framework/Models/MonitorColorScheme.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// A monitor color scheme to use. + internal enum MonitorColorScheme + { + /// Choose a color scheme automatically. + AutoDetect, + + /// Use lighter text colors that look better on a black or dark background. + DarkBackground, + + /// Use darker text colors that look better on a white or light background. + LightBackground + } +} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 2d6da0fa..b504f38b 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -20,5 +20,8 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } + + /// The console color scheme to use. + public MonitorColorScheme ColorScheme { get; set; } } } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index bf338386..da025ab9 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework { @@ -25,7 +26,7 @@ namespace StardewModdingAPI.Framework private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); /// The console text color for each log level. - private static readonly IDictionary Colors = Monitor.GetConsoleColorScheme(); + private readonly IDictionary Colors; /// Propagates notification that SMAPI should exit. private readonly CancellationTokenSource ExitTokenSource; @@ -58,13 +59,15 @@ namespace StardewModdingAPI.Framework /// Manages access to the console output. /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource) + /// The console color scheme to use. + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) { // validate if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); // initialise + this.Colors = Monitor.GetConsoleColorScheme(colorScheme); this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleManager = consoleManager; @@ -76,7 +79,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { - this.LogImpl(this.Source, message, level, Monitor.Colors[level]); + this.LogImpl(this.Source, message, level, this.Colors[level]); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. @@ -145,32 +148,41 @@ namespace StardewModdingAPI.Framework } /// Get the color scheme to use for the current console. - private static IDictionary GetConsoleColorScheme() + /// The console color scheme to use. + private static IDictionary GetConsoleColorScheme(MonitorColorScheme colorScheme) { - // scheme for dark console background - if (Monitor.IsDark(Console.BackgroundColor)) - { - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; - } + // auto detect color scheme + if (colorScheme == MonitorColorScheme.AutoDetect) + colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; - // scheme for light console background - return new Dictionary + // get colors for scheme + switch (colorScheme) { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.Black, - [LogLevel.Warn] = ConsoleColor.DarkYellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.DarkMagenta - }; + case MonitorColorScheme.DarkBackground: + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warn] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.Magenta + }; + + case MonitorColorScheme.LightBackground: + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.Black, + [LogLevel.Warn] = ConsoleColor.DarkYellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.DarkMagenta + }; + + default: + throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); + } } /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. @@ -182,6 +194,7 @@ namespace StardewModdingAPI.Framework case ConsoleColor.Black: case ConsoleColor.Blue: case ConsoleColor.DarkBlue: + case ConsoleColor.DarkMagenta: // Powershell case ConsoleColor.DarkRed: case ConsoleColor.Red: return true; diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index da2c0e8e..36310fed 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -134,7 +134,7 @@ namespace StardewModdingAPI // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) { WriteToConsole = writeToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, @@ -1149,7 +1149,7 @@ namespace StardewModdingAPI /// The name of the module which will log messages with this instance. private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 9743894a..06c63e8b 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -36,5 +36,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * Whether SMAPI should log more information about the game context. */ - "VerboseLogging": false + "VerboseLogging": false, + + /** + * The console color theme to use. The possible values are: + * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Mac. + * - LightBackground: use darker text colors that look better on a white or light background. + * - DarkBackground: use lighter text colors that look better on a black or dark background. + */ + "ColorScheme": "AutoDetect" } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9dbca475..6e9d0d9c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -123,6 +123,7 @@ + -- cgit From 45f4f85b7e74e0cffd345310d6aabc95c12dac26 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 23:47:24 -0400 Subject: add MacOS detection --- build/common.targets | 9 ++ docs/release-notes.md | 1 + src/SMAPI.Common/EnvironmentUtility.cs | 97 ++++++++++++++++++++++ src/SMAPI.Common/Platform.cs | 15 ++++ .../StardewModdingAPI.Common.projitems | 2 + src/SMAPI.Installer/Enums/Platform.cs | 12 --- src/SMAPI.Installer/InteractiveInstaller.cs | 28 ++----- .../StardewModdingAPI.Installer.csproj | 1 - src/SMAPI/Constants.cs | 11 +-- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 1 + src/SMAPI/Framework/ModLoading/Platform.cs | 12 --- .../Framework/ModLoading/PlatformAssemblyMap.cs | 1 + src/SMAPI/Program.cs | 22 +---- src/SMAPI/StardewModdingAPI.csproj | 2 - 15 files changed, 140 insertions(+), 76 deletions(-) create mode 100644 src/SMAPI.Common/EnvironmentUtility.cs create mode 100644 src/SMAPI.Common/Platform.cs delete mode 100644 src/SMAPI.Installer/Enums/Platform.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Platform.cs (limited to 'src') diff --git a/build/common.targets b/build/common.targets index a6cead8f..95885299 100644 --- a/build/common.targets +++ b/build/common.targets @@ -26,6 +26,10 @@ $(DefineConstants);SMAPI_FOR_WINDOWS + + + + False @@ -38,6 +42,8 @@ False + + $(GamePath)\Netcode.dll False @@ -58,11 +64,14 @@ $(DefineConstants);SMAPI_FOR_UNIX + $(GamePath)\MonoGame.Framework.dll False False + + $(GamePath)\StardewValley.exe False diff --git a/docs/release-notes.md b/docs/release-notes.md index 817fcd47..40f404d3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * Fixed issue where assets didn't reload correctly when the player switches language. * For SMAPI developers: + * Added MacOS detection to `Constants.Platform`. * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. diff --git a/src/SMAPI.Common/EnvironmentUtility.cs b/src/SMAPI.Common/EnvironmentUtility.cs new file mode 100644 index 00000000..b8a6d775 --- /dev/null +++ b/src/SMAPI.Common/EnvironmentUtility.cs @@ -0,0 +1,97 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if SMAPI_FOR_WINDOWS +using System.Management; +#endif +using System.Runtime.InteropServices; + +namespace StardewModdingAPI.Common +{ + /// Provides methods for fetching environment information. + internal static class EnvironmentUtility + { + /********* + ** Properties + *********/ + /// Get the OS name from the system uname command. + /// The buffer to fill with the resulting string. + [DllImport("libc")] + static extern int uname(IntPtr buffer); + + + /********* + ** Public methods + *********/ + /// Detect the current OS. + public static Platform DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return Platform.Mac; + + case PlatformID.Unix: + return EnvironmentUtility.IsRunningMac() + ? Platform.Mac + : Platform.Linux; + + default: + return Platform.Windows; + } + } + + + /// Get the human-readable OS name and version. + /// The current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + public static string GetFriendlyPlatformName(Platform platform) + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; + } + + + /********* + ** Private methods + *********/ + /// Detect whether the code is running on Mac. + /// + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the + /// uname system command and checking the response, which is always 'Darwin' for MacOS. + /// + private static bool IsRunningMac() + { + IntPtr buffer = IntPtr.Zero; + try + { + buffer = Marshal.AllocHGlobal(8192); + if (uname(buffer) == 0) + { + string os = Marshal.PtrToStringAnsi(buffer); + return os == "Darwin"; + } + return false; + } + catch + { + return false; // default to Linux + } + finally + { + if (buffer != IntPtr.Zero) + Marshal.FreeHGlobal(buffer); + } + } + } +} diff --git a/src/SMAPI.Common/Platform.cs b/src/SMAPI.Common/Platform.cs new file mode 100644 index 00000000..08b4545f --- /dev/null +++ b/src/SMAPI.Common/Platform.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Common +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux version of the game. + Linux, + + /// The Mac version of the game. + Mac, + + /// The Windows version of the game. + Windows + } +} diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems index 223b0d5c..0b89f092 100644 --- a/src/SMAPI.Common/StardewModdingAPI.Common.projitems +++ b/src/SMAPI.Common/StardewModdingAPI.Common.projitems @@ -9,6 +9,8 @@ StardewModdingAPI.Common + + diff --git a/src/SMAPI.Installer/Enums/Platform.cs b/src/SMAPI.Installer/Enums/Platform.cs deleted file mode 100644 index 9bcaa3c3..00000000 --- a/src/SMAPI.Installer/Enums/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingApi.Installer.Enums -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux/Mac version of the game. - Mono, - - /// The Windows version of the game. - Windows - } -} \ No newline at end of file diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index b7e698ad..01076573 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -27,7 +27,8 @@ namespace StardewModdingApi.Installer { switch (platform) { - case Platform.Mono: + case Platform.Linux: + case Platform.Mac: { string home = Environment.GetEnvironmentVariable("HOME"); @@ -140,7 +141,7 @@ namespace StardewModdingApi.Installer /**** ** Get platform & set window title ****/ - Platform platform = this.DetectPlatform(); + Platform platform = EnvironmentUtility.DetectPlatform(); Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform}"; Console.WriteLine(); @@ -182,7 +183,7 @@ namespace StardewModdingApi.Installer DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); var paths = new { - executable = Path.Combine(installDir.FullName, platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe"), + executable = Path.Combine(installDir.FullName, platform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"), unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"), unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") @@ -271,7 +272,7 @@ namespace StardewModdingApi.Installer ** Always uninstall old files ****/ // restore game launcher - if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup)) + if ((platform == Platform.Linux || platform == Platform.Mac) && File.Exists(paths.unixLauncherBackup)) { this.PrintDebug("Removing SMAPI launcher..."); this.InteractivelyDelete(paths.unixLauncher); @@ -304,7 +305,7 @@ namespace StardewModdingApi.Installer } // replace mod launcher (if possible) - if (platform == Platform.Mono) + if (platform == Platform.Linux || platform == Platform.Mac) { this.PrintDebug("Safely replacing game launcher..."); if (!File.Exists(paths.unixLauncherBackup)) @@ -379,21 +380,6 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ - /// Detect the game's platform. - /// The platform is not supported. - private Platform DetectPlatform() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - case PlatformID.Unix: - return Platform.Mono; - - default: - return Platform.Windows; - } - } - /// Test whether the current console supports color formatting. private static bool GetConsoleSupportsColor() { @@ -635,7 +621,7 @@ namespace StardewModdingApi.Installer // normalise path if (platform == Platform.Windows) path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path - if (platform == Platform.Mono) + if (platform == Platform.Linux || platform == Platform.Mac) path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line if (path.StartsWith("~/")) { diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index a575e06f..7a71bef9 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -40,7 +40,6 @@ Properties\GlobalAssemblyInfo.cs - diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a098194b..1faaf656 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewValley; @@ -91,12 +92,7 @@ namespace StardewModdingAPI internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// The target game platform. - internal static Platform TargetPlatform { get; } = -#if SMAPI_FOR_WINDOWS - Platform.Windows; -#else - Platform.Mono; -#endif + internal static Platform TargetPlatform { get; } = EnvironmentUtility.DetectPlatform(); /********* @@ -111,7 +107,8 @@ namespace StardewModdingAPI Assembly[] targetAssemblies; switch (targetPlatform) { - case Platform.Mono: + case Platform.Linux: + case Platform.Mac: removeAssemblyReferences = new[] { "Stardew Valley", diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 533da398..d95de4fe 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; -using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewValley; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index a60f63da..bf9b6450 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Metadata; diff --git a/src/SMAPI/Framework/ModLoading/Platform.cs b/src/SMAPI/Framework/ModLoading/Platform.cs deleted file mode 100644 index 45e881c4..00000000 --- a/src/SMAPI/Framework/ModLoading/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux/Mac version of the game. - Mono, - - /// The Windows version of the game. - Windows - } -} diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index 463f45e8..9499b538 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; +using StardewModdingAPI.Common; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 36310fed..27f7b4d5 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -10,10 +10,10 @@ using System.Security; using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS -using System.Management; using System.Windows.Forms; #endif using Newtonsoft.Json; +using StardewModdingAPI.Common; using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; @@ -165,7 +165,7 @@ namespace StardewModdingAPI try { // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.TargetPlatform)}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); @@ -1157,24 +1157,6 @@ namespace StardewModdingAPI }; } - /// Get a human-readable name for the current platform. - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] - private string GetFriendlyPlatformName() - { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - return Environment.OSVersion.ToString(); - } - /// Log a message if verbose mode is enabled. /// The message to log. private void VerboseLog(string message) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6e9d0d9c..9b4a496e 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -72,7 +72,6 @@ - True @@ -110,7 +109,6 @@ - -- cgit From 0d5278a270a0d51baefe99cf908f532a66489602 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Apr 2018 23:54:44 -0400 Subject: use light-background color scheme on Mac by default --- docs/release-notes.md | 2 +- src/SMAPI/Framework/Monitor.cs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 40f404d3..9ae1d2c4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,7 +5,7 @@ * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. - * Fixed console color scheme for PowerShell and added override option to `StardewModdingAPI.config.json`. + * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. * For modders: * Added code analysis to mod build config package to flag common issues as warnings. diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index da025ab9..a76afc3c 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; @@ -153,7 +154,12 @@ namespace StardewModdingAPI.Framework { // auto detect color scheme if (colorScheme == MonitorColorScheme.AutoDetect) - colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; + { + if (Constants.TargetPlatform == Platform.Mac) + colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white. + else + colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; + } // get colors for scheme switch (colorScheme) -- cgit From 6d269621b28240a0da6de5fd5faa4c35553c15bb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 00:18:19 -0400 Subject: make crossplatform handling more consistent --- docs/release-notes.md | 3 ++- src/SMAPI.Common/EnvironmentUtility.cs | 15 +++++++++++++++ src/SMAPI.Installer/InteractiveInstaller.cs | 21 ++++++++------------- 3 files changed, 25 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ae1d2c4..f3e9af6f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,7 +14,8 @@ * Fixed issue where assets didn't reload correctly when the player switches language. * For SMAPI developers: - * Added MacOS detection to `Constants.Platform`. + * Added more consistent crossplatform handling using a new `EnvironmentUtility`. + * Added MacOS detection. * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. diff --git a/src/SMAPI.Common/EnvironmentUtility.cs b/src/SMAPI.Common/EnvironmentUtility.cs index b8a6d775..9d9e91e6 100644 --- a/src/SMAPI.Common/EnvironmentUtility.cs +++ b/src/SMAPI.Common/EnvironmentUtility.cs @@ -61,6 +61,21 @@ namespace StardewModdingAPI.Common return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; } + /// Get the name of the Stardew Valley executable. + /// The current platform. + public static string GetExecutableName(Platform platform) + { + return platform == Platform.Windows + ? "Stardew Valley.exe" + : "StardewValley.exe"; + } + + /// Get whether the platform uses Mono. + /// The current platform. + public static bool IsMono(this Platform platform) + { + return platform == Platform.Linux || platform == Platform.Mac; + } /********* ** Private methods diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 01076573..0d602b57 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -142,7 +142,7 @@ namespace StardewModdingApi.Installer ** Get platform & set window title ****/ Platform platform = EnvironmentUtility.DetectPlatform(); - Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform}"; + Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.WriteLine(); /**** @@ -179,11 +179,11 @@ namespace StardewModdingApi.Installer } // get folders - DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.ToString())); + DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.IsMono() ? "Mono" : "Windows")); DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); var paths = new { - executable = Path.Combine(installDir.FullName, platform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"), + executable = Path.Combine(installDir.FullName, EnvironmentUtility.GetExecutableName(platform)), unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"), unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") @@ -199,7 +199,7 @@ namespace StardewModdingApi.Installer { this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip") ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder." - : $"The 'internal/{platform}' package folder is missing (should be at {packageDir})." + : $"The 'internal/{packageDir.Name}' package folder is missing (should be at {packageDir})." ); Console.ReadLine(); return; @@ -272,7 +272,7 @@ namespace StardewModdingApi.Installer ** Always uninstall old files ****/ // restore game launcher - if ((platform == Platform.Linux || platform == Platform.Mac) && File.Exists(paths.unixLauncherBackup)) + if (platform.IsMono() && File.Exists(paths.unixLauncherBackup)) { this.PrintDebug("Removing SMAPI launcher..."); this.InteractivelyDelete(paths.unixLauncher); @@ -305,7 +305,7 @@ namespace StardewModdingApi.Installer } // replace mod launcher (if possible) - if (platform == Platform.Linux || platform == Platform.Mac) + if (platform.IsMono()) { this.PrintDebug("Safely replacing game launcher..."); if (!File.Exists(paths.unixLauncherBackup)) @@ -554,9 +554,7 @@ namespace StardewModdingApi.Installer private DirectoryInfo InteractivelyGetInstallPath(Platform platform, string specifiedPath) { // get executable name - string executableFilename = platform == Platform.Windows - ? "Stardew Valley.exe" - : "StardewValley.exe"; + string executableFilename = EnvironmentUtility.GetExecutableName(platform); // validate specified path if (specifiedPath != null) @@ -662,10 +660,7 @@ namespace StardewModdingApi.Installer string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); // get path - string homePath = platform == Platform.Windows - ? Environment.GetEnvironmentVariable("APPDATA") - : Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".config"); - string appDataPath = Path.Combine(homePath, "StardewValley"); + string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods")); // check if migration needed -- cgit From 6616c87c1806159ce6e751d4ccd54b5500d8b903 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 00:24:24 -0400 Subject: fix world_settime command sometimes breaking NPC schedules --- docs/release-notes.md | 1 + .../Framework/Commands/World/SetTimeCommand.cs | 34 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index f3e9af6f..e524ea65 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. + * Fixed `world_settime` console command sometimes breaking NPC schedules (e.g. so they stay in bed). * For modders: * Added code analysis to mod build config package to flag common issues as warnings. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index d6c71387..7644ee46 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -31,9 +32,38 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World return; // handle - Game1.timeOfDay = time; + this.SafelySetTime(time); FreezeTimeCommand.FrozenTime = Game1.timeOfDay; monitor.Log($"OK, the time is now {Game1.timeOfDay.ToString().PadLeft(4, '0')}.", LogLevel.Info); } + + + /********* + ** Private methods + *********/ + /// Safely transition to the given time, allowing NPCs to update their schedule. + /// The time of day. + private void SafelySetTime(int time) + { + // define conversion between game time and TimeSpan + TimeSpan ToTimeSpan(int value) => new TimeSpan(0, value / 100, value % 100, 0); + int FromTimeSpan(TimeSpan span) => (int)((span.Hours * 100) + span.Minutes); + + // transition to new time + int intervals = (int)((ToTimeSpan(time) - ToTimeSpan(Game1.timeOfDay)).TotalMinutes / 10); + if (intervals > 0) + { + for (int i = 0; i < intervals; i++) + Game1.performTenMinuteClockUpdate(); + } + else if (intervals < 0) + { + for (int i = 0; i > intervals; i--) + { + Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 mins so game updates to next interval + Game1.performTenMinuteClockUpdate(); + } + } + } } } -- cgit From 052ef9683a473ce2253f56ca6323abbc70ba9d76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 02:40:55 -0400 Subject: fix typo in code docs (#473) --- src/SMAPI/Events/EventArgsInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index b2bd978b..0cf0828b 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Events this.IsUseToolButton = isUseToolButton; } - /// Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event. + /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. public void SuppressButton() { this.SuppressButton(this.Button); -- cgit From c2cb76b79919261c4d7eab107c5cb77ec6c6a81c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 17:53:58 -0400 Subject: rewrite analyzers to match more cases, use readable warning IDs (#471) --- docs/mod-build-config.md | 12 +-- .../NetFieldAnalyzerTests.cs | 8 +- .../ObsoleteFieldAnalyzerTests.cs | 5 +- .../AnalyzerUtilities.cs | 46 +++++++++++ .../NetFieldAnalyzer.cs | 95 ++++++---------------- .../ObsoleteFieldAnalyzer.cs | 21 +++-- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 7 files changed, 93 insertions(+), 96 deletions(-) create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 99a567f2..d942beeb 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -153,8 +153,8 @@ You can hide the warnings... See below for help with each specific warning. -### SMAPI001 -**Implicit net field conversion:** +### Avoid implicit net field cast +Warning text: > This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but > {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual > value instead to avoid bugs. @@ -185,8 +185,8 @@ Suggested fix: if (item != null && item.category.Value == 0) ``` -### SMAPI002 -**Avoid net fields when possible:** +### Avoid net field +Warning text: > '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead. Your code accesses a net field, which has some unusual behavior (see [SMAPI001](#smapi001)). This @@ -194,8 +194,8 @@ field has an equivalent non-net property that avoids those issues. Suggested fix: access the suggested property name instead. -### SMAPI003 -**Avoid obsolete fields:** +### Avoid obsolete field +Warning text: > The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'. Your code accesses a field which is obsolete or no longer works. Use the suggested field instead. diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 101f4c21..79ce9263 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -91,8 +91,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); DiagnosticResult expected = new DiagnosticResult { - Id = "SMAPI001", - Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/smapi001 for details.", + Id = "AvoidImplicitNetFieldCast", + Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/avoid-implicit-net-field-cast for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) } }; @@ -117,8 +117,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); DiagnosticResult expected = new DiagnosticResult { - Id = "SMAPI002", - Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/smapi002 for details.", + Id = "AvoidNetField", + Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) } }; diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs index dc7476ef..102a80d1 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs @@ -59,14 +59,15 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests /// The old field name which should be reported. /// The new field name which should be reported. [TestCase("var x = new Farmer().friendships;", 8, "StardewValley.Farmer.friendships", "friendshipData")] + [TestCase("var x = new Farmer()?.friendships;", 8, "StardewValley.Farmer.friendships", "friendshipData")] public void AvoidObsoleteField_RaisesDiagnostic(string codeText, int column, string oldName, string newName) { // arrange string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); DiagnosticResult expected = new DiagnosticResult { - Id = "SMAPI003", - Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/smapi003 for details.", + Id = "AvoidObsoleteField", + Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", ObsoleteFieldAnalyzerTests.SampleCodeLine, ObsoleteFieldAnalyzerTests.SampleCodeColumn + column) } }; diff --git a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs new file mode 100644 index 00000000..77e7812f --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StardewModdingAPI.ModBuildConfig.Analyzer +{ + /// Provides generic utilities for SMAPI's Roslyn analyzers. + internal static class AnalyzerUtilities + { + /********* + ** Public methods + *********/ + /// Get the metadata for a member access expression. + /// The member access expression. + /// provides methods for asking semantic questions about syntax nodes. + /// The object type which has the member. + /// The type of the accessed member. + /// The name of the accessed member. + /// Returns true if the node is a member access expression, else false. + public static bool GetMemberInfo(SyntaxNode node, SemanticModel semanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName) + { + // simple access + if (node is MemberAccessExpressionSyntax memberAccess) + { + declaringType = semanticModel.GetTypeInfo(memberAccess.Expression).Type; + memberType = semanticModel.GetTypeInfo(node); + memberName = memberAccess.Name.Identifier.Text; + return true; + } + + // conditional access + if (node is ConditionalAccessExpressionSyntax conditionalAccess && conditionalAccess.WhenNotNull is MemberBindingExpressionSyntax conditionalBinding) + { + declaringType = semanticModel.GetTypeInfo(conditionalAccess.Expression).Type; + memberType = semanticModel.GetTypeInfo(node); + memberName = conditionalBinding.Name.Identifier.Text; + return true; + } + + // invalid + declaringType = null; + memberType = default(TypeInfo); + memberName = null; + return false; + } + } +} diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 915a50e8..895eebf0 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace StardewModdingAPI.ModBuildConfig.Analyzer @@ -127,23 +126,23 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// Describes the diagnostic rule covered by the analyzer. private readonly IDictionary Rules = new Dictionary { - ["SMAPI001"] = new DiagnosticDescriptor( - id: "SMAPI001", + ["AvoidImplicitNetFieldCast"] = new DiagnosticDescriptor( + id: "AvoidImplicitNetFieldCast", title: "Netcode types shouldn't be implicitly converted", - messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/smapi001 for details.", + messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/avoid-implicit-net-field-cast for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/smapi001" + helpLinkUri: "https://smapi.io/buildmsg/avoid-implicit-net-field-cast" ), - ["SMAPI002"] = new DiagnosticDescriptor( - id: "SMAPI002", + ["AvoidNetField"] = new DiagnosticDescriptor( + id: "AvoidNetField", title: "Avoid Netcode types when possible", - messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/smapi002 for details.", + messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/smapi001" + helpLinkUri: "https://smapi.io/buildmsg/avoid-net-field" ) }; @@ -170,19 +169,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer { // SMAPI002: avoid net fields if possible context.RegisterSyntaxNodeAction( - this.AnalyzeAvoidableNetField, - SyntaxKind.SimpleMemberAccessExpression - ); - - // SMAPI001: avoid implicit net field conversion - context.RegisterSyntaxNodeAction( - this.AnalyseNetFieldConversions, - SyntaxKind.EqualsExpression, - SyntaxKind.NotEqualsExpression, - SyntaxKind.GreaterThanExpression, - SyntaxKind.GreaterThanOrEqualExpression, - SyntaxKind.LessThanExpression, - SyntaxKind.LessThanOrEqualExpression + this.AnalyzeMemberAccess, + SyntaxKind.SimpleMemberAccessExpression, + SyntaxKind.ConditionalAccessExpression ); } @@ -192,68 +181,30 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer *********/ /// Analyse a syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available. /// The analysis context. - private void AnalyzeAvoidableNetField(SyntaxNodeAnalysisContext context) + private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) { try { - // check member type - MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node; - TypeInfo memberType = context.SemanticModel.GetTypeInfo(node); + // get member access info + if (!AnalyzerUtilities.GetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) + return; if (!this.IsNetType(memberType.Type)) return; + bool isConverted = !this.IsNetType(memberType.ConvertedType); - // get reference info - ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type; - string propertyName = node.Name.Identifier.Text; - - // suggest replacement + // warn: use property wrapper if available for (ITypeSymbol type = declaringType; type != null; type = type.BaseType) { - if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{propertyName}", out string suggestedPropertyName)) + if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{memberName}", out string suggestedPropertyName)) { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI002"], context.Node.GetLocation(), node, memberType.Type.Name, suggestedPropertyName)); - break; + context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidNetField"], context.Node.GetLocation(), context.Node, memberType.Type.Name, suggestedPropertyName)); + return; } } - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); - } - } - /// Analyse a syntax node and add a diagnostic message if it implicitly converts a net field. - /// The analysis context. - private void AnalyseNetFieldConversions(SyntaxNodeAnalysisContext context) - { - try - { - BinaryExpressionSyntax binaryExpression = (BinaryExpressionSyntax)context.Node; - foreach (var pair in new[] { Tuple.Create(binaryExpression.Left, binaryExpression.Right), Tuple.Create(binaryExpression.Right, binaryExpression.Left) }) - { - // get node info - ExpressionSyntax curExpression = pair.Item1; // the side of the comparison being examined - ExpressionSyntax otherExpression = pair.Item2; // the other side - TypeInfo typeInfo = context.SemanticModel.GetTypeInfo(curExpression); - if (!this.IsNetType(typeInfo.Type)) - continue; - - // warn for implicit conversion - if (!this.IsNetType(typeInfo.ConvertedType)) - { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI001"], context.Node.GetLocation(), curExpression, typeInfo.Type.Name, typeInfo.ConvertedType)); - break; - } - - // warn for comparison to null - // An expression like `building.indoors != null` will sometimes convert `building.indoors` to NetFieldBase instead of object before comparison. Haven't reproduced this in unit tests yet. - Optional otherValue = context.SemanticModel.GetConstantValue(otherExpression); - if (otherValue.HasValue && otherValue.Value == null) - { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI001"], context.Node.GetLocation(), curExpression, typeInfo.Type.Name, "null")); - break; - } - } + // warn: implicit conversion + if (isConverted) + context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); } catch (Exception ex) { diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index 00565329..dc21e505 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace StardewModdingAPI.ModBuildConfig.Analyzer @@ -25,14 +24,14 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// Describes the diagnostic rule covered by the analyzer. private readonly IDictionary Rules = new Dictionary { - ["SMAPI003"] = new DiagnosticDescriptor( - id: "SMAPI003", + ["AvoidObsoleteField"] = new DiagnosticDescriptor( + id: "AvoidObsoleteField", title: "Reference to obsolete field", - messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/smapi003 for details.", + messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/smapi003" + helpLinkUri: "https://smapi.io/buildmsg/avoid-obsolete-field" ) }; @@ -60,7 +59,8 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer // SMAPI003: avoid obsolete fields context.RegisterSyntaxNodeAction( this.AnalyzeObsoleteFields, - SyntaxKind.SimpleMemberAccessExpression + SyntaxKind.SimpleMemberAccessExpression, + SyntaxKind.ConditionalAccessExpression ); } @@ -75,16 +75,15 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer try { // get reference info - MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node; - ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type; - string propertyName = node.Name.Identifier.Text; + if (!AnalyzerUtilities.GetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) + return; // suggest replacement for (ITypeSymbol type = declaringType; type != null; type = type.BaseType) { - if (this.ReplacedFields.TryGetValue($"{type}::{propertyName}", out string replacement)) + if (this.ReplacedFields.TryGetValue($"{type}::{memberName}", out string replacement)) { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI003"], context.Node.GetLocation(), $"{type}.{propertyName}", replacement)); + context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidObsoleteField"], context.Node.GetLocation(), $"{type}.{memberName}", replacement)); break; } } diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 92e7e81e..2512c4d6 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1-alpha20180410 + 2.1-alpha20180414 Build package for SMAPI mods Pathoschild Pathoschild -- cgit From 6d8cf614a24ab69baffa89c351b9a22776741442 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 19:51:50 -0400 Subject: don't warn for NetList conversion to implemented interface (#471) --- .../Mock/Netcode/NetList.cs | 9 +++++++ .../Mock/Netcode/NetObjectList.cs | 6 +++++ .../Mock/StardewValley/Farmer.cs | 11 +++++++- .../NetFieldAnalyzerTests.cs | 19 +++++++++++++- .../AnalyzerUtilities.cs | 12 +++++++++ .../NetFieldAnalyzer.cs | 30 +++++++++++++++++++--- .../ObsoleteFieldAnalyzer.cs | 2 +- 7 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs new file mode 100644 index 00000000..1699f71c --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs @@ -0,0 +1,9 @@ +// ReSharper disable CheckNamespace -- matches Stardew Valley's code +using System.Collections; +using System.Collections.Generic; + +namespace Netcode +{ + /// A simplified version of Stardew Valley's Netcode.NetObjectList for unit testing. + public class NetList : List, IList, ICollection, IEnumerable, IEnumerable { } +} diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs new file mode 100644 index 00000000..7814e7d6 --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs @@ -0,0 +1,6 @@ +// ReSharper disable CheckNamespace -- matches Stardew Valley's code +namespace Netcode +{ + /// A simplified version of Stardew Valley's Netcode.NetObjectList for unit testing. + public class NetObjectList : NetList { } +} diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs index e0f0e30c..54e91682 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs @@ -1,11 +1,20 @@ // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code +#pragma warning disable 649 // (never assigned) -- only used to test type conversions using System.Collections.Generic; +using Netcode; namespace StardewValley { /// A simplified version of Stardew Valley's StardewValley.Farmer class for unit testing. internal class Farmer { - public IDictionary friendships; + /// A sample field which should be replaced with a different property. + public readonly IDictionary friendships; + + /// A sample net list. + public readonly NetList eventsSeen; + + /// A sample net object list. + public readonly NetObjectList netObjectList; } } diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 79ce9263..15bcadcd 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -19,6 +19,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests using StardewValley; using Netcode; using SObject = StardewValley.Object; + using SFarmer = StardewValley.Farmer; namespace SampleMod { @@ -33,7 +34,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests "; /// The line number where the unit tested code is injected into . - private const int SampleCodeLine = 13; + private const int SampleCodeLine = 14; /// The column number where the unit tested code is injected into . private const int SampleCodeColumn = 25; @@ -85,6 +86,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests [TestCase("SObject obj = null; if (obj.netRefField != null);", 24, "obj.netRefField", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefProperty == null);", 24, "obj.netRefProperty", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")] + [TestCase("SFarmer farmer = new SFarmer(); object list = farmer.eventsSeen;", 46, "farmer.eventsSeen", "NetList", "object")] // ↓ NetList field converted to a non-interface type public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType) { // arrange @@ -101,6 +103,21 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests this.VerifyCSharpDiagnostic(code, expected); } + /// Test that the net field analyzer doesn't raise any warnings for safe member access. + /// The code line to test. + [TestCase("SFarmer farmer = new SFarmer(); System.Collections.IEnumerable list = farmer.eventsSeen;")] + [TestCase("SFarmer farmer = new SFarmer(); System.Collections.Generic.IEnumerable list = farmer.eventsSeen;")] + [TestCase("SFarmer farmer = new SFarmer(); System.Collections.Generic.IList list = farmer.eventsSeen;")] + [TestCase("SFarmer farmer = new SFarmer(); System.Collections.Generic.IList list = farmer.netObjectList;")] // subclass of NetList + public void AvoidImplicitNetFieldComparisons_AllowsSafeAccess(string codeText) + { + // arrange + string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); + + // assert + this.VerifyCSharpDiagnostic(code); + } + /// Test that the expected diagnostic message is raised for avoidable net field references. /// The code line to test. /// The column within the code line where the diagnostic message should be reported. diff --git a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs index 77e7812f..e0c0cd63 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -42,5 +43,16 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer memberName = null; return false; } + + /// Get the class types in a type's inheritance chain, including itself. + /// The initial type. + public static IEnumerable GetConcreteTypes(ITypeSymbol type) + { + while (type != null) + { + yield return type; + type = type.BaseType; + } + } } } diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 895eebf0..7c8b804e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -17,6 +19,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// The namespace for Stardew Valley's Netcode types. private const string NetcodeNamespace = "Netcode"; + /// The full name for Stardew Valley's Netcode.NetList type. + private readonly string NetListTypeFullName = "Netcode.NetList"; + /// Maps net fields to their equivalent non-net properties where available. private readonly IDictionary NetFieldWrapperProperties = new Dictionary { @@ -190,10 +195,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer return; if (!this.IsNetType(memberType.Type)) return; - bool isConverted = !this.IsNetType(memberType.ConvertedType); // warn: use property wrapper if available - for (ITypeSymbol type = declaringType; type != null; type = type.BaseType) + foreach (ITypeSymbol type in AnalyzerUtilities.GetConcreteTypes(declaringType)) { if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{memberName}", out string suggestedPropertyName)) { @@ -203,7 +207,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer } // warn: implicit conversion - if (isConverted) + if (this.IsInvalidConversion(memberType)) context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); } catch (Exception ex) @@ -212,6 +216,26 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer } } + /// Get whether a net field was converted in an error-prone way. + /// The member access type info. + private bool IsInvalidConversion(TypeInfo typeInfo) + { + // no conversion + if (!this.IsNetType(typeInfo.Type) || this.IsNetType(typeInfo.ConvertedType)) + return false; + + // list conversion to an implemented interface is OK + if (AnalyzerUtilities.GetConcreteTypes(typeInfo.Type).Any(p => p.ToString().StartsWith(this.NetListTypeFullName))) // StartsWith to ignore generics + { + string toType = typeInfo.ConvertedType.ToString(); + if (toType.StartsWith(typeof(IEnumerable<>).Namespace) || toType == typeof(IEnumerable).FullName) + return false; + } + + // avoid any other conversions + return true; + } + /// Get whether a type symbol references a Netcode type. /// The type symbol. private bool IsNetType(ITypeSymbol typeSymbol) diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index dc21e505..943d0350 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -79,7 +79,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer return; // suggest replacement - for (ITypeSymbol type = declaringType; type != null; type = type.BaseType) + foreach (ITypeSymbol type in AnalyzerUtilities.GetConcreteTypes(declaringType)) { if (this.ReplacedFields.TryGetValue($"{type}::{memberName}", out string replacement)) { -- cgit From 1848abe7d57e32207db9535c9c83d96dbda64ced Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 20:14:31 -0400 Subject: don't warn for NetCollection conversion to implemented interface (#471) --- .../Mock/Netcode/NetCollection.cs | 10 ++++++++++ .../Mock/StardewValley/Farmer.cs | 7 ------- .../Mock/StardewValley/Item.cs | 9 +++++++++ .../NetFieldAnalyzerTests.cs | 12 +++++++----- src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs | 11 +++++++++++ 5 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs new file mode 100644 index 00000000..d160610e --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs @@ -0,0 +1,10 @@ +// ReSharper disable CheckNamespace -- matches Stardew Valley's code +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Netcode +{ + /// A simplified version of Stardew Valley's Netcode.NetCollection for unit testing. + public class NetCollection : Collection, IList, ICollection, IEnumerable, IEnumerable { } +} diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs index 54e91682..13fab069 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs @@ -1,7 +1,6 @@ // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code #pragma warning disable 649 // (never assigned) -- only used to test type conversions using System.Collections.Generic; -using Netcode; namespace StardewValley { @@ -10,11 +9,5 @@ namespace StardewValley { /// A sample field which should be replaced with a different property. public readonly IDictionary friendships; - - /// A sample net list. - public readonly NetList eventsSeen; - - /// A sample net object list. - public readonly NetObjectList netObjectList; } } diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs index 386767d7..1b6317c1 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs @@ -20,5 +20,14 @@ namespace StardewValley /// A generic net ref property with no equivalent non-net property. public NetRef netRefProperty { get; } = new NetRef(); + + /// A sample net list. + public readonly NetList netList = new NetList(); + + /// A sample net object list. + public readonly NetObjectList netObjectList = new NetObjectList(); + + /// A sample net collection. + public readonly NetCollection netCollection = new NetCollection(); } } diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 15bcadcd..7b410085 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -86,7 +86,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests [TestCase("SObject obj = null; if (obj.netRefField != null);", 24, "obj.netRefField", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefProperty == null);", 24, "obj.netRefProperty", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")] - [TestCase("SFarmer farmer = new SFarmer(); object list = farmer.eventsSeen;", 46, "farmer.eventsSeen", "NetList", "object")] // ↓ NetList field converted to a non-interface type + [TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type + [TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")] public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType) { // arrange @@ -105,10 +106,11 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests /// Test that the net field analyzer doesn't raise any warnings for safe member access. /// The code line to test. - [TestCase("SFarmer farmer = new SFarmer(); System.Collections.IEnumerable list = farmer.eventsSeen;")] - [TestCase("SFarmer farmer = new SFarmer(); System.Collections.Generic.IEnumerable list = farmer.eventsSeen;")] - [TestCase("SFarmer farmer = new SFarmer(); System.Collections.Generic.IList list = farmer.eventsSeen;")] - [TestCase("SFarmer farmer = new SFarmer(); System.Collections.Generic.IList list = farmer.netObjectList;")] // subclass of NetList + [TestCase("Item item = new Item(); System.Collections.IEnumerable list = farmer.eventsSeen;")] + [TestCase("Item item = new Item(); System.Collections.Generic.IEnumerable list = farmer.netList;")] + [TestCase("Item item = new Item(); System.Collections.Generic.IList list = farmer.netList;")] + [TestCase("Item item = new Item(); System.Collections.Generic.ICollection list = farmer.netCollection;")] + [TestCase("Item item = new Item(); System.Collections.Generic.IList list = farmer.netObjectList;")] // subclass of NetList public void AvoidImplicitNetFieldComparisons_AllowsSafeAccess(string codeText) { // arrange diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 7c8b804e..72d3bbf8 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -22,6 +22,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// The full name for Stardew Valley's Netcode.NetList type. private readonly string NetListTypeFullName = "Netcode.NetList"; + /// The full name for Stardew Valley's Netcode.NetCollection type. + private readonly string NetCollectionTypeFullName = "Netcode.NetCollection"; + /// Maps net fields to their equivalent non-net properties where available. private readonly IDictionary NetFieldWrapperProperties = new Dictionary { @@ -232,6 +235,14 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer return false; } + // collection conversion to an implemented interface is OK + if (AnalyzerUtilities.GetConcreteTypes(typeInfo.Type).Any(p => p.ToString().StartsWith(this.NetCollectionTypeFullName))) // StartsWith to ignore generics + { + string toType = typeInfo.ConvertedType.ToString(); + if (toType.StartsWith(typeof(IEnumerable<>).Namespace) || toType == typeof(IEnumerable).FullName) + return false; + } + // avoid any other conversions return true; } -- cgit From 97120c6df2f59a12eef0129f13d773bdde305184 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Apr 2018 20:33:43 -0400 Subject: update references to old warning IDs (#471) --- docs/mod-build-config.md | 12 ++++++------ docs/screenshots/code-analyzer-example.png | Bin 4022 -> 3473 bytes src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs | 1 - .../ObsoleteFieldAnalyzer.cs | 1 - src/SMAPI/Framework/SGame.cs | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index d942beeb..74ee34e4 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -140,11 +140,11 @@ To enable it, add this above the first `` in your `.csproj`: The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example: ![](screenshots/code-analyzer-example.png) -You can hide the warnings... +You can hide the warnings using the warning ID (shown under 'code' in the Error List). See... * [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning); * for a method using this attribute: ```cs - [System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "SMAPI001")] // implicit net field conversion + [System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")] ``` * for an entire project: 1. Expand the _References_ node for the project in Visual Studio. @@ -163,11 +163,11 @@ Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplaye can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`), but their conversion rules are unintuitive and error-prone. For example, `item?.category == null && item?.category != null` can both be true at once, and -`building.indoors != null` will be true for a null value in some cases. +`building.indoors != null` can be true for a null value. Suggested fix: * Some net fields have an equivalent non-net property like `monster.Health` (`int`) instead of - `monster.health` (`NetInt`). The package will add a separate [SMAPI002](#smapi002) warning for + `monster.health` (`NetInt`). The package will add a separate [AvoidNetField](#avoid-net-field) warning for these. Use the suggested property instead. * For a reference type (i.e. one that can contain `null`), you can use the `.Value` property: ```c# @@ -189,8 +189,8 @@ Suggested fix: Warning text: > '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead. -Your code accesses a net field, which has some unusual behavior (see [SMAPI001](#smapi001)). This -field has an equivalent non-net property that avoids those issues. +Your code accesses a net field, which has some unusual behavior (see [AvoidImplicitNetFieldCast](#avoid-implicit-net-field-cast)). +This field has an equivalent non-net property that avoids those issues. Suggested fix: access the suggested property name instead. diff --git a/docs/screenshots/code-analyzer-example.png b/docs/screenshots/code-analyzer-example.png index 3b930dc5..de38f643 100644 Binary files a/docs/screenshots/code-analyzer-example.png and b/docs/screenshots/code-analyzer-example.png differ diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 72d3bbf8..e3c92617 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -175,7 +175,6 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// The analysis context. public override void Initialize(AnalysisContext context) { - // SMAPI002: avoid net fields if possible context.RegisterSyntaxNodeAction( this.AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression, diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index 943d0350..a770f47d 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -56,7 +56,6 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// The analysis context. public override void Initialize(AnalysisContext context) { - // SMAPI003: avoid obsolete fields context.RegisterSyntaxNodeAction( this.AnalyzeObsoleteFields, SyntaxKind.SimpleMemberAccessExpression, diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 67390882..be98aeb1 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -633,7 +633,7 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] - [SuppressMessage("SMAPI.CommonErrors", "SMAPI002", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")] private void DrawImpl(GameTime gameTime) { if (Game1.debugMode) -- cgit From bb2c52386015829c161a56d418f3795335559d8a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 15 Apr 2018 01:14:28 -0400 Subject: tweak trace logs for readability when loading mods --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 14 +++++++------- src/SMAPI/Program.cs | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index bf9b6450..feaee047 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -99,13 +99,13 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); // load assembly if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (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); @@ -116,7 +116,7 @@ namespace StardewModdingAPI.Framework.ModLoading else { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } @@ -290,22 +290,22 @@ namespace StardewModdingAPI.Framework.ModLoading case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", #if SMAPI_FOR_WINDOWS this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug #else diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 27f7b4d5..f70efb89 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -712,12 +712,12 @@ namespace StardewModdingAPI { // get basic info IManifest manifest = metadata.Manifest; - this.Monitor.Log($"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)} (content pack)...", LogLevel.Trace); + this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)})...", LogLevel.Trace); // validate status if (metadata.Status == ModMetadataStatus.Failed) { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); TrackSkip(metadata, metadata.Error); continue; } @@ -755,13 +755,13 @@ namespace StardewModdingAPI // get basic info IManifest manifest = metadata.Manifest; this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid - : $"Loading {metadata.DisplayName}...", LogLevel.Trace); + ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid + : $" {metadata.DisplayName}...", LogLevel.Trace); // validate status if (metadata.Status == ModMetadataStatus.Failed) { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); TrackSkip(metadata, metadata.Error); continue; } -- cgit From 7f4941167e58f27f43b073db2e37d14b7fee2543 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Sun, 15 Apr 2018 22:13:26 -0500 Subject: initial player_add changes --- .../Framework/Commands/Player/AddCommand.cs | 96 ++++++++++++++++++---- 1 file changed, 81 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index ff11da1e..09b9585c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; @@ -30,24 +31,25 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { - // read arguments - if (!args.TryGet(0, "item type", out string rawType, oneOf: Enum.GetNames(typeof(ItemType)))) - return; - if (!args.TryGetInt(1, "item ID", out int id, min: 0)) + SearchableItem match; + int optionalArgStartIndex; + + if (!args.TryGet(0, "item type or name", out string typeOrName, required: true)) return; - if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) - count = 1; - if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) - quality = Object.lowQuality; - ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true); - // find matching item - SearchableItem match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); + // read arguments + if (Enum.GetNames(typeof(ItemType)).Contains(typeOrName, StringComparer.InvariantCultureIgnoreCase)) + this.FindItemByTypeAndId(monitor, args, typeOrName, out optionalArgStartIndex, out match); + else + this.FindItemByName(monitor, args, typeOrName, out optionalArgStartIndex, out match); + if (match == null) - { - monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); return; - } + + if (!args.TryGetInt(optionalArgStartIndex, "count", out int count, min: 1, required: false)) + count = 1; + if (!args.TryGetInt(optionalArgStartIndex + 1, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) + quality = Object.lowQuality; // apply count match.Item.Stack = count; @@ -63,6 +65,69 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); } + /// + /// Finds a matching item by item type and id. + /// + /// Writes messages to the console and log file. + /// The command arguments. + /// The raw item type. + /// The index of subsequent arguments. + /// The matching item. + private void FindItemByTypeAndId(IMonitor monitor, ArgumentParser args, string rawType, out int nextArgIndex, out SearchableItem match) + { + match = null; + nextArgIndex = 2; + + // read arguments + if (!args.TryGetInt(1, "item ID", out int id, min: 0)) + return; + + ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true); + + // find matching item + match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); + + if (match == null) + { + monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); + } + } + + /// + /// Finds a matching item by name. + /// + /// Writes messages to the console and log file. + /// The command arguments. + /// The item name. + /// The index of subsequent arguments. + /// The matching item. + private void FindItemByName(IMonitor monitor, ArgumentParser args, string name, out int nextArgIndex, out SearchableItem match) + { + match = null; + nextArgIndex = 1; + + // interpret names with underscores as spaces + name = name.Replace('_', ' '); + + // find matching items + IEnumerable matching = this.Items.GetAll().Where(p => p.DisplayName.Equals(name, StringComparison.CurrentCultureIgnoreCase)); + int numberOfMatches = matching.Count(); + + // handle unique requirement + if (numberOfMatches == 0) + { + monitor.Log($"There's no item with name {name}.", LogLevel.Error); + } + else if (numberOfMatches == 1) + { + match = matching.ElementAt(0); + } + else + { + monitor.Log($"There are {numberOfMatches} items with name {name}. Try specifying a type and id instead.", LogLevel.Error); + } + } + /********* ** Private methods *********/ @@ -71,9 +136,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player string[] typeValues = Enum.GetNames(typeof(ItemType)); return "Gives the player an item.\n" + "\n" - + "Usage: player_add [count] [quality]\n" + + "Usage: player_add ( |) [count] [quality]\n" + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" + "- item: the item ID (use the 'list_items' command to see a list).\n" + + "- name: the display name of the item (only if there is exactly one such item).\n" + "- count (optional): how many of the item to give.\n" + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + "\n" -- cgit From a6e1ea0a4f87c621edc74d789eb76ce70893b5d1 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 16 Apr 2018 00:42:57 -0500 Subject: fix private method comment location --- .../Framework/Commands/Player/AddCommand.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 09b9585c..6c48d8df 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -65,6 +65,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); } + /********* + ** Private methods + *********/ + /// /// Finds a matching item by item type and id. /// @@ -128,9 +132,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player } } - /********* - ** Private methods - *********/ private static string GetDescription() { string[] typeValues = Enum.GetNames(typeof(ItemType)); -- cgit From d362843706c012867324dc9723e7c7a49a7a2621 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 16 Apr 2018 03:15:25 -0500 Subject: add Name as new type, show more helpful info on multiple matches --- .../Framework/Commands/Player/AddCommand.cs | 43 +++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 6c48d8df..47ba9e8d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -16,6 +16,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Provides methods for searching and constructing items. private readonly ItemRepository Items = new ItemRepository(); + private readonly string[] ItemTypeAndName = Enum.GetNames(typeof(ItemType)).Union(new string[] { "Name" }).ToArray(); /********* ** Public methods @@ -32,23 +33,21 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player public override void Handle(IMonitor monitor, string command, ArgumentParser args) { SearchableItem match; - int optionalArgStartIndex; - if (!args.TryGet(0, "item type or name", out string typeOrName, required: true)) + //read arguments + if (!args.TryGet(0, "item type", out string typeOrName, oneOf: this.ItemTypeAndName)) return; - - // read arguments if (Enum.GetNames(typeof(ItemType)).Contains(typeOrName, StringComparer.InvariantCultureIgnoreCase)) - this.FindItemByTypeAndId(monitor, args, typeOrName, out optionalArgStartIndex, out match); + this.FindItemByTypeAndId(monitor, args, typeOrName, out match); else - this.FindItemByName(monitor, args, typeOrName, out optionalArgStartIndex, out match); - + this.FindItemByName(monitor, args, out match); + if (match == null) return; - if (!args.TryGetInt(optionalArgStartIndex, "count", out int count, min: 1, required: false)) + if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) count = 1; - if (!args.TryGetInt(optionalArgStartIndex + 1, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) + if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) quality = Object.lowQuality; // apply count @@ -75,12 +74,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Writes messages to the console and log file. /// The command arguments. /// The raw item type. - /// The index of subsequent arguments. /// The matching item. - private void FindItemByTypeAndId(IMonitor monitor, ArgumentParser args, string rawType, out int nextArgIndex, out SearchableItem match) + private void FindItemByTypeAndId(IMonitor monitor, ArgumentParser args, string rawType, out SearchableItem match) { match = null; - nextArgIndex = 2; // read arguments if (!args.TryGetInt(1, "item ID", out int id, min: 0)) @@ -103,18 +100,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Writes messages to the console and log file. /// The command arguments. /// The item name. - /// The index of subsequent arguments. /// The matching item. - private void FindItemByName(IMonitor monitor, ArgumentParser args, string name, out int nextArgIndex, out SearchableItem match) + private void FindItemByName(IMonitor monitor, ArgumentParser args, out SearchableItem match) { match = null; - nextArgIndex = 1; - // interpret names with underscores as spaces - name = name.Replace('_', ' '); + // read arguments + if (!args.TryGet(1, "item name", out string name)) + return; // find matching items - IEnumerable matching = this.Items.GetAll().Where(p => p.DisplayName.Equals(name, StringComparison.CurrentCultureIgnoreCase)); + IEnumerable matching = this.Items.GetAll().Where(p => p.DisplayName.IndexOf(name, StringComparison.InvariantCultureIgnoreCase) != -1); int numberOfMatches = matching.Count(); // handle unique requirement @@ -128,7 +124,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player } else { - monitor.Log($"There are {numberOfMatches} items with name {name}. Try specifying a type and id instead.", LogLevel.Error); + string options = this.GetTableString(matching, new string[] { "type", "name", "command" }, item => new string[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" }); + + monitor.Log($"Found multiple item names containing '{name}'. Type one of these commands for the one you want:", LogLevel.Error); + monitor.Log($"\n{options}", LogLevel.Info); } } @@ -137,10 +136,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player string[] typeValues = Enum.GetNames(typeof(ItemType)); return "Gives the player an item.\n" + "\n" - + "Usage: player_add ( |) [count] [quality]\n" - + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" + + "Usage: player_add (|) [count] [quality]\n" + + $"- type: the item type (either Name or one of {string.Join(", ", typeValues)}).\n" + "- item: the item ID (use the 'list_items' command to see a list).\n" - + "- name: the display name of the item (only if there is exactly one such item).\n" + + "- name: the display name of the item (when using type Name).\n" + "- count (optional): how many of the item to give.\n" + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + "\n" -- cgit From 96753c35fd3d6baed73a933c552e5f0a5a8fa02c Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 16 Apr 2018 03:39:08 -0500 Subject: add world ready check and more helpful error messages --- .../Framework/Commands/Player/AddCommand.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 47ba9e8d..3d55b425 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -32,6 +32,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { + // validate + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + SearchableItem match; //read arguments @@ -111,16 +118,22 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // find matching items IEnumerable matching = this.Items.GetAll().Where(p => p.DisplayName.IndexOf(name, StringComparison.InvariantCultureIgnoreCase) != -1); + SearchableItem exactMatch = matching.FirstOrDefault(item => item.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + int numberOfMatches = matching.Count(); // handle unique requirement - if (numberOfMatches == 0) + if (exactMatch != null) { - monitor.Log($"There's no item with name {name}.", LogLevel.Error); + match = matching.ElementAt(0); + } + else if (numberOfMatches == 0) + { + monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error); } else if (numberOfMatches == 1) { - match = matching.ElementAt(0); + monitor.Log($"There's no item with name '{name}'. Did you mean '{matching.ElementAt(0).DisplayName}'? If so, type 'player_add name {matching.ElementAt(0).DisplayName}'.", LogLevel.Error); } else { -- cgit From b990f81eda42395eff63d77093092663de751712 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 16 Apr 2018 22:39:33 -0500 Subject: support quoted strings in console commands --- src/SMAPI/Framework/CommandManager.cs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 79a23d03..78e03827 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Framework if (string.IsNullOrWhiteSpace(input)) return false; - string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + string[] args = this.ParseArgs(input); string name = args[0]; args = args.Skip(1).ToArray(); @@ -103,6 +103,32 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// + /// Parses a string into an array of arguments. + /// + /// The string to parse. + private string[] ParseArgs(string input) + { + bool inQuotes = false; + IList args = new List(); + IList currentArg = new List(); + foreach (char c in input) + { + if (c == '"') + { + inQuotes = !inQuotes; + } + else if (!inQuotes && char.IsWhiteSpace(c)) + { + args.Add(string.Concat(currentArg)); + currentArg.Clear(); + } + else + currentArg.Add(c); + } + return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); + } + /// Get a normalised command name. /// The command name. private string GetNormalisedName(string name) -- cgit From 43487a40e391978221df00ba86f2b7628ed8d343 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Tue, 17 Apr 2018 15:35:22 -0500 Subject: refactor finding items by name slightly --- .../Framework/Commands/Player/AddCommand.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 3d55b425..453b8e32 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -118,16 +118,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // find matching items IEnumerable matching = this.Items.GetAll().Where(p => p.DisplayName.IndexOf(name, StringComparison.InvariantCultureIgnoreCase) != -1); - SearchableItem exactMatch = matching.FirstOrDefault(item => item.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase)); - - int numberOfMatches = matching.Count(); + match = matching.FirstOrDefault(item => item.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase)); // handle unique requirement - if (exactMatch != null) + if (match != null) { - match = matching.ElementAt(0); + return; } - else if (numberOfMatches == 0) + + int numberOfMatches = matching.Count(); + + if (numberOfMatches == 0) { monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error); } -- cgit From 4af998024cf47ed90a2177c42b77217208685f50 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 17 Apr 2018 21:12:03 -0400 Subject: add warning in developer mode for mods with no update keys --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 9 +++++++++ src/SMAPI/Program.cs | 8 ++++++++ 4 files changed, 21 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e524ea65..cde7847e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. + * Added warning when a mod doesn't have update keys (currently only shown in developer mode). * Dropped some deprecated APIs. * Fixed assets loaded by temporary content managers not being editable. * Fixed issue where assets didn't reload correctly when the player switches language. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index d1e8eb7d..248809df 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -64,5 +64,8 @@ namespace StardewModdingAPI.Framework /// Set the mod-provided API instance. /// The mod-provided API. IModMetadata SetApi(object api); + + /// Whether the mod has at least one update key set. + bool HasUpdateKeys(); } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 1a0f9994..af888b71 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading @@ -102,5 +103,13 @@ namespace StardewModdingAPI.Framework.ModLoading this.Api = api; return this; } + + /// Whether the mod has at least one update key set. + public bool HasUpdateKeys() + { + return + this.Manifest?.UpdateKeys != null + && this.Manifest.UpdateKeys.Any(key => !string.IsNullOrWhiteSpace(key)); + } } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f70efb89..cf1c082a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -722,6 +722,10 @@ namespace StardewModdingAPI continue; } + // show warnings + if (this.Settings.DeveloperMode && !metadata.HasUpdateKeys()) + this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + // load mod as content pack IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); @@ -766,6 +770,10 @@ namespace StardewModdingAPI continue; } + // show warnings + if (this.Settings.DeveloperMode && !metadata.HasUpdateKeys() && metadata.Manifest.UniqueID != "SMAPI.ConsoleCommands") + this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + // load mod string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) -- cgit From 73b75c628666f52120975ed70e572bba0e2810c3 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Tue, 17 Apr 2018 21:46:30 -0500 Subject: log user input to log file --- src/SMAPI/Framework/Monitor.cs | 25 ++++++++++++++++++++++--- src/SMAPI/Program.cs | 3 +++ 2 files changed, 25 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index a76afc3c..b65932b2 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -100,6 +100,17 @@ namespace StardewModdingAPI.Framework this.LogFile.WriteLine(""); } + /// Writes user input to the log file. + /// The input to write. + internal void LogUserInputToFile(string input) + { + if (this.WriteToFile) + { + string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); + this.LogFile.WriteLine($"{prefix} $>{input}"); + } + } + /********* ** Private methods @@ -120,9 +131,8 @@ namespace StardewModdingAPI.Framework private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) { // generate message - string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); - - string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; + string prefix = this.GenerateMessagePrefix(source, level); + string fullMessage = $"{prefix} {message}"; string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; // write to console @@ -148,6 +158,15 @@ namespace StardewModdingAPI.Framework this.LogFile.WriteLine(fullMessage); } + /// Generates a message prefix for the current time. + /// The name of the mod logging the message. + /// The log level. + private string GenerateMessagePrefix(string source, LogLevel level) + { + string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); + return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]"; + } + /// Get the color scheme to use for the current console. /// The console color scheme to use. private static IDictionary GetConsoleColorScheme(MonitorColorScheme colorScheme) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f70efb89..063707af 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -444,6 +444,9 @@ namespace StardewModdingAPI if (string.IsNullOrWhiteSpace(input)) continue; + // write input to log file + this.Monitor.LogUserInputToFile(input); + // parse input try { -- cgit From c79601ad76c25c58e3810a746cfff1be6edba1e1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Apr 2018 20:22:50 -0400 Subject: update for Stardew Valley 1.3.0.32 (#453) --- src/SMAPI/Framework/ContentCore.cs | 46 ++++++++----------------------- src/SMAPI/Framework/ContentManagerShim.cs | 5 ++-- src/SMAPI/Framework/SGame.cs | 19 ++++--------- 3 files changed, 20 insertions(+), 50 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index 9da18481..ef001f79 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -47,9 +47,6 @@ namespace StardewModdingAPI.Framework /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. private readonly IDictionary IsLocalisableLookup; - /// The locale codes used in asset keys indexed by enum value. - private readonly IDictionary Locales; - /// The language enum values indexed by locale code. private readonly IDictionary LanguageCodes; @@ -94,21 +91,19 @@ namespace StardewModdingAPI.Framework /// The service provider to use to locate services. /// The root directory to search for content. /// The current culture for which to localise content. - /// The current language code for which to localise content. /// Encapsulates monitoring and logging. /// Simplifies access to private code. - public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) + public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection) { // init this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture, languageCodeOverride); + this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture); this.Cache = new ContentCache(this.Content, reflection); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); // get asset data this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection); - this.Locales = this.GetKeyLocales(reflection); - this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); this.IsLocalisableLookup = reflection.GetField>(this.Content, "_localizedAsset").GetValue(); } @@ -117,7 +112,7 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content (or null. for the default) public ContentManagerShim CreateContentManager(string name, string rootDirectory = null) { - return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture, this.Content.LanguageCodeOverride); + return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture); } /**** @@ -177,7 +172,7 @@ namespace StardewModdingAPI.Framework /// The language. public string GetLocale(LocalizedContentManager.LanguageCode language) { - return this.Locales[language]; + return this.Content.LanguageCodeString(language); } /// Get whether the content manager has already loaded and cached the given asset. @@ -413,31 +408,14 @@ namespace StardewModdingAPI.Framework } /// Get the locale codes (like ja-JP) used in asset keys. - /// Simplifies access to private game code. - private IDictionary GetKeyLocales(Reflector reflection) + private IDictionary GetKeyLocales() { - string previousOverride = this.Content.LanguageCodeOverride; - - try - { - // temporarily disable language override - this.Content.LanguageCodeOverride = null; + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) + map[code] = this.Content.LanguageCodeString(code); - // create locale => code map - IReflectedMethod languageCodeString = reflection.GetMethod(this.Content, "languageCodeString"); - IDictionary map = new Dictionary(); - foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) - { - map[code] = languageCodeString.Invoke(code); - } - - return map; - } - finally - { - // restore previous settings - this.Content.LanguageCodeOverride = previousOverride; - } + return map; } /// Get the asset name from a cache key. @@ -486,7 +464,7 @@ namespace StardewModdingAPI.Framework return false; return localisable - ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}") + ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.Content.GetCurrentLanguage())}") : this.Cache.ContainsKey(normalisedAssetName); } diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs index 2791eb78..66754fd7 100644 --- a/src/SMAPI/Framework/ContentManagerShim.cs +++ b/src/SMAPI/Framework/ContentManagerShim.cs @@ -30,9 +30,8 @@ namespace StardewModdingAPI.Framework /// The service provider to use to locate services. /// The root directory to search for content. /// The current culture for which to localise content. - /// The current language code for which to localise content. - public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride) - : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) + public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture) + : base(serviceProvider, rootDirectory, currentCulture) { this.ContentCore = contentCore; this.Name = name; diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index be98aeb1..c33aa084 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -131,15 +131,8 @@ namespace StardewModdingAPI.Framework /// A callback to invoke after the game finishes initialising. private readonly Action OnGameInitialised; - /**** - ** Private wrappers - ****/ /// Simplifies access to private game code. - private static Reflector Reflection; - - // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming - private static StringBuilder _debugStringBuilder => SGame.Reflection.GetField(typeof(Game1), nameof(_debugStringBuilder)).GetValue(); - // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + private readonly Reflector Reflection; /********* @@ -166,10 +159,10 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.Events = eventManager; this.FirstUpdate = true; - SGame.Reflection = reflection; + this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case - this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); + this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); // set XNA option required by Stardew Valley Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -187,7 +180,7 @@ namespace StardewModdingAPI.Framework // Don't depend on anything being initialised at this point. if (this.ContentCore == null) { - this.ContentCore = new ContentCore(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, null, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); + this.ContentCore = new ContentCore(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); SGame.MonitorDuringInitialisation = null; } return this.ContentCore.CreateContentManager("(generated)", rootDirectory); @@ -609,7 +602,7 @@ namespace StardewModdingAPI.Framework // recover sprite batch try { - if (Game1.spriteBatch.IsOpen(SGame.Reflection)) + if (Game1.spriteBatch.IsOpen(this.Reflection)) { this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); Game1.spriteBatch.End(); @@ -1206,7 +1199,7 @@ namespace StardewModdingAPI.Framework overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); if (Game1.debugMode) { - StringBuilder debugStringBuilder = SGame._debugStringBuilder; + StringBuilder debugStringBuilder = Game1._debugStringBuilder; debugStringBuilder.Clear(); if (Game1.panMode) { -- cgit From e80e6564b374b9b5a37748f1a42e10025d92776f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Apr 2018 20:58:25 -0400 Subject: detect broken references to fields which changed generic type (#453) Previously generic types couldn't be compared correctly, since we'd end up with false differences like "Dictionary`1 != Dictionary". That seems to be fixed now, possibly due to the PDB file being included. --- .../ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index b5e45742..2cbb3a8e 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -59,10 +59,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - // can't compare generic type parameters between definition and reference - if (fieldRef.FieldType.IsGenericInstance || fieldRef.FieldType.IsGenericParameter) - return InstructionHandleResult.None; - // get target field FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); if (targetField == null) -- cgit From 8faf409ea1485c05b8812fd2ff226200561708b5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Apr 2018 21:06:56 -0400 Subject: update compatibility list for first test pass (#453) --- src/SMAPI/StardewModdingAPI.metadata.json | 265 +++++++++++++----------------- 1 file changed, 110 insertions(+), 155 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 237c069d..07a63a20 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -47,6 +47,9 @@ * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it * even if it detects incompatible code). * + * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility + * automatically, since that hides the details from trace logs. + * * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded * (if applicable). If blank, will default to a generic not-compatible message. * @@ -65,8 +68,7 @@ "ID": "ThatNorthernMonkey.AdjustArtisanPrices", "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update "MapRemoteVersions": { "0.01": "0.0.1" }, - "Default | UpdateKey": "Chucklefish:3532", - "~0.0.1 | Status": "AssumeBroken" + "Default | UpdateKey": "Chucklefish:3532" }, "Adjust Monster": { @@ -76,8 +78,7 @@ "Advanced Location Loader": { "ID": "Entoarox.AdvancedLocationLoader", - "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) - "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~1.3.7 | UpdateKey": "Chucklefish:3619" // only enable update checks up to 1.3.7 by request (has its own update-check feature) }, "Adventure Shop Inventory": { @@ -103,12 +104,11 @@ "Default | UpdateKey": "Nexus:174" }, - "Almighty Tool": { + "Almighty Farming Tool": { "ID": "439", "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 "MapRemoteVersions": { "1.21": "1.2.1" }, - "Default | UpdateKey": "Nexus:439", - "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:439" }, "Animal Husbandry": { @@ -137,8 +137,7 @@ "A Tapper's Dream": { "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "Default | UpdateKey": "Nexus:260", - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:260" }, "Auto Animal Doors": { @@ -164,7 +163,8 @@ "Automate": { "ID": "Pathoschild.Automate", - "Default | UpdateKey": "Nexus:1063" + "Default | UpdateKey": "Nexus:1063", + "~1.9.1 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Automated Doors": { @@ -208,15 +208,13 @@ "Better Shipping Box": { "ID": "Kithio:BetterShippingBox", "MapLocalVersions": { "1.0.1": "1.0.2" }, - "Default | UpdateKey": "Chucklefish:4302", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Chucklefish:4302" }, "Better Sprinklers": { "ID": "Speeder.BetterSprinklers", "FormerIDs": "SPDSprinklersMod", // changed in 2.3 - "Default | UpdateKey": "Nexus:41", - "~2.3.1-pathoschild-update | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:41" }, "Billboard Anywhere": { @@ -228,8 +226,7 @@ "Birthday Mail": { "ID": "KathrynHazuka.BirthdayMail", "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update - "Default | UpdateKey": "Nexus:276", - "~1.2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:276" }, "Breed Like Rabbits": { @@ -240,28 +237,24 @@ "Build Endurance": { "ID": "Omegasis.BuildEndurance", "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:445", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:445" // added in 1.4.1 }, "Build Health": { "ID": "Omegasis.BuildHealth", "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Default | UpdateKey": "Nexus:446", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:446" // added in 1.4.1 }, "Buy Cooking Recipes": { "ID": "Denifia.BuyRecipes", - "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:1126" // added in 1.0.1 (2017-10-04) }, "Buy Back Collectables": { "ID": "Omegasis.BuyBackCollectables", "FormerIDs": "BuyBackCollectables", // changed in 1.4 - "Default | UpdateKey": "Nexus:507", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:507" // added in 1.4.1 }, "Carry Chest": { @@ -289,22 +282,20 @@ "Chest Label System": { "ID": "Speeder.ChestLabel", "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update - "Default | UpdateKey": "Nexus:242", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.1 + "Default | UpdateKey": "Nexus:242" }, "Chest Pooling": { "ID": "mralbobo.ChestPooling", "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 - "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling" }, "Chests Anywhere": { "ID": "Pathoschild.ChestsAnywhere", "FormerIDs": "ChestsAnywhere", // changed in 1.9 "Default | UpdateKey": "Nexus:518", - "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 + "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Choose Baby Gender": { @@ -324,21 +315,21 @@ "ID": "CJBok.CheatsMenu", "FormerIDs": "CJBCheatsMenu", // changed in 1.14 "Default | UpdateKey": "Nexus:4", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 + "~1.17 | Status": "AssumeBroken" // broke in SDV 1.3 }, "CJB Item Spawner": { "ID": "CJBok.ItemSpawner", "FormerIDs": "CJBItemSpawner", // changed in 1.7 "Default | UpdateKey": "Nexus:93", - "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 + "~1.10 | Status": "AssumeBroken" // broke in SDV 1.3 }, "CJB Show Item Sell Price": { "ID": "CJBok.ShowItemSellPrice", "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 "Default | UpdateKey": "Nexus:5", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Clean Farm": { @@ -348,8 +339,7 @@ "Climates of Ferngill": { "ID": "KoihimeNakamura.ClimatesOfFerngill", - "Default | UpdateKey": "Nexus:604", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:604" }, "Coal Regen": { @@ -371,8 +361,7 @@ "Combat with Farm Implements": { "ID": "SPDFarmingImplementsInCombat", - "Default | UpdateKey": "Nexus:313", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:313" }, "Community Bundle Item Tooltip": { @@ -397,18 +386,22 @@ "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "Content Patcher": { + "ID": "Pathoschild.ContentPatcher", + "Default | UpdateKey": "Nexus:1915", + "~1.3.1 | Status": "AssumeBroken" // broke in SDV 1.3 (in-game errors) + }, + "Cooking Skill": { "ID": "spacechase0.CookingSkill", "FormerIDs": "CookingSkill", // changed in 1.0.4–6 - "Default | UpdateKey": "Nexus:522", - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:522" }, "CrabNet": { "ID": "jwdred.CrabNet", "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 - "Default | UpdateKey": "Nexus:584", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:584" }, "Crafting Counter": { @@ -477,8 +470,7 @@ "Customizable Traveling Cart Days": { "ID": "TravelingCartYyeahdude", - "Default | UpdateKey": "Nexus:567", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:567" }, "Custom Linens": { @@ -519,10 +511,17 @@ "Default | UpdateKey": "Nexus:513" // added in 1.4.1 }, + "Data Maps": { + "ID": "Pathoschild.DataMaps", + "Default | UpdateKey": "Nexus:1691", + "~1.3 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + "Debug Mode": { "ID": "Pathoschild.DebugMode", "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 - "Default | UpdateKey": "Nexus:679" + "Default | UpdateKey": "Nexus:679", + "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Did You Water Your Crops?": { @@ -532,8 +531,7 @@ "Dynamic Checklist": { "ID": "gunnargolf.DynamicChecklist", - "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:1145" // added in 1.0.1-pathoschild-update }, "Dynamic Horses": { @@ -561,15 +559,13 @@ "Empty Hands": { "ID": "QuicksilverFox.EmptyHands", - "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:1176" // added in 1.0.1-pathoschild-update }, "Enemy Health Bars": { "ID": "Speeder.HealthBars", "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update - "Default | UpdateKey": "Nexus:193", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:193" }, "Entoarox Framework": { @@ -598,8 +594,7 @@ "Extended Fridge": { "ID": "Crystalmir.ExtendedFridge", "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 - "Default | UpdateKey": "Nexus:485", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:485" }, "Extended Greenhouse": { @@ -644,8 +639,7 @@ "Farm Expansion": { "ID": "Advize.FarmExpansion", "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 - "Default | UpdateKey": "Nexus:130", - "~2.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:130" }, "Farm Resource Generator": { @@ -656,7 +650,8 @@ "Fast Animations": { "ID": "Pathoschild.FastAnimations", - "Default | UpdateKey": "Nexus:1089" + "Default | UpdateKey": "Nexus:1089", + "~1.5 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Faster Grass": { @@ -673,8 +668,7 @@ "Faster Run": { "ID": "KathrynHazuka.FasterRun", "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update - "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:733" // added in 1.1.1-pathoschild-update }, "Fishing Adjust": { @@ -707,8 +701,7 @@ "Forage at the Farm": { "ID": "Nishtra.ForageAtTheFarm", "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 - "Default | UpdateKey": "Nexus:673", - "~1.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:673" }, "Furniture Anywhere": { @@ -725,14 +718,12 @@ "Gate Opener": { "ID": "mralbobo.GateOpener", "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 - "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener" }, "GenericShopExtender": { "ID": "GenericShopExtender", - "Default | UpdateKey": "Nexus:814", // added in 0.1.3 - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:814" // added in 0.1.3 }, "Geode Info Menu": { @@ -743,8 +734,7 @@ "Get Dressed": { "ID": "Advize.GetDressed", "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 - "Default | UpdateKey": "Nexus:331", - "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:331" }, "Giant Crop Ring": { @@ -755,8 +745,7 @@ "Gift Taste Helper": { "ID": "tstaples.GiftTasteHelper", "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 - "Default | UpdateKey": "Nexus:229", - "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:229" }, "Grandfather's Gift": { @@ -772,8 +761,7 @@ "Happy Birthday (Omegasis)": { "ID": "Omegasis.HappyBirthday", "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:520", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:520" // added in 1.4.1 }, "Happy Birthday (Oxyligen fork)": { @@ -798,8 +786,7 @@ "Harvest With Scythe": { "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "Default | UpdateKey": "Nexus:236", - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:236" }, "Horse Whistle (icepuente)": { @@ -849,8 +836,7 @@ "Interaction Helper": { "ID": "HammurabiInteractionHelper", - "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Chucklefish:4640" // added in 1.0.4-pathoschild-update }, "Item Auto Stacker": { @@ -910,7 +896,7 @@ "ID": "Pathoschild.LookupAnything", "FormerIDs": "LookupAnything", // changed in 1.10.1 "Default | UpdateKey": "Nexus:541", - "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 + "~1.18.1 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Love Bubbles": { @@ -921,15 +907,13 @@ "Loved Labels": { "ID": "Advize.LovedLabels", "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 - "Default | UpdateKey": "Nexus:279", - "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:279" }, "Luck Skill": { "ID": "spacechase0.LuckSkill", "FormerIDs": "LuckSkill", // changed in 0.1.4 - "Default | UpdateKey": "Nexus:521", - "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:521" }, "Mail Framework": { @@ -940,15 +924,13 @@ "MailOrderPigs": { "ID": "jwdred.MailOrderPigs", "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:632", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:632" }, "Makeshift Multiplayer": { "ID": "spacechase0.StardewValleyMP", "FormerIDs": "StardewValleyMP", // changed in 0.3 - "Default | UpdateKey": "Nexus:501", - "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:501" }, "Map Image Exporter": { @@ -989,14 +971,12 @@ "More Animals": { "ID": "Entoarox.MoreAnimals", "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 - "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) - "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility + "~2.0.2 | UpdateKey": "Chucklefish:4288" // only enable update checks up to 2.0.2 by request (has its own update-check feature) }, "More Artifact Spots": { "ID": "451", - "Default | UpdateKey": "Nexus:451", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:451" }, "More Map Layers": { @@ -1031,8 +1011,7 @@ "Museum Rearranger": { "ID": "Omegasis.MuseumRearranger", "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:428", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:428" // added in 1.4.1 }, "Mushroom Level Tip": { @@ -1050,8 +1029,7 @@ "ID": "Omegasis.NightOwl", "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest - "Default | UpdateKey": "Nexus:433", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:433" // added in 1.4.1 }, "No Crows": { @@ -1099,9 +1077,7 @@ "NPC Map Locations": { "ID": "NPCMapLocationsMod", - "Default | UpdateKey": "Nexus:239", - "1.42~1.43 | Status": "AssumeBroken", - "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." + "Default | UpdateKey": "Nexus:239" }, "NPC Speak": { @@ -1118,8 +1094,12 @@ "OmniFarm": { "ID": "PhthaloBlue.OmniFarm", "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", - "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm" + }, + + "One Click Shed": { + "ID": "BitwiseJonMods.OneClickShedReloader", + "Default | UpdateKey": "Nexus:2052" }, "Out of Season Bonuses (Seasonal Items)": { @@ -1129,22 +1109,18 @@ "Part of the Community": { "ID": "SB_PotC", - "Default | UpdateKey": "Nexus:923", - "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:923" }, "PelicanFiber": { "ID": "jwdred.PelicanFiber", "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 - "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version - "Default | UpdateKey": "Nexus:631", - "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:631" }, "PelicanTTS": { "ID": "Platonymous.PelicanTTS", - "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 - "~1.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:1079" // added in 1.6.1 }, "Persia the Mermaid - Standalone Custom NPC": { @@ -1249,8 +1225,7 @@ "Replanter": { "ID": "jwdred.Replanter", "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 - "Default | UpdateKey": "Nexus:589", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:589" }, "ReRegeneration": { @@ -1288,21 +1263,20 @@ "Rotate Toolbar": { "ID": "Pathoschild.RotateToolbar", - "Default | UpdateKey": "Nexus:1100" + "Default | UpdateKey": "Nexus:1100", + "~1.2.1 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Rush Orders": { "ID": "spacechase0.RushOrders", "FormerIDs": "RushOrders", // changed in 1.1 - "Default | UpdateKey": "Nexus:605", - "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:605" }, "Save Anywhere": { "ID": "Omegasis.SaveAnywhere", "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl - "Default | UpdateKey": "Nexus:444", // added in 2.6.1 - "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:444" // added in 2.6.1 }, "Save Backup": { @@ -1331,8 +1305,7 @@ "Seasonal Immersion": { "ID": "Entoarox.SeasonalImmersion", "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) - "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~1.11 | UpdateKey": "Chucklefish:4262" // only enable update checks up to 1.11 by request (has its own update-check feature) }, "Seed Bag": { @@ -1353,8 +1326,7 @@ "Send Items": { "ID": "Denifia.SendItems", - "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:1087" // added in 1.0.3 (2017-10-04) }, "Shed Notifications (BuildingsNotifications)": { @@ -1367,8 +1339,7 @@ "ID": "Nishtra.ShenandoahProject", "FormerIDs": "Shenandoah Project", // changed in 1.2 "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest - "Default | UpdateKey": "Nexus:756", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:756" }, "Ship Anywhere": { @@ -1378,16 +1349,14 @@ "Shipment Tracker": { "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "Default | UpdateKey": "Nexus:321", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:321" }, "Shop Expander": { "ID": "Entoarox.ShopExpander", "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths "MapRemoteVersions": { "1.6.0b": "1.6.0" }, - "~1.6 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.6 by request (has its own update-check feature) - "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature) }, "Showcase Mod": { @@ -1409,43 +1378,39 @@ "Simple Sound Manager": { "ID": "Omegasis.SimpleSoundManager", - "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:1410" // added in 1.0.1 }, "Simple Sprinklers": { "ID": "tZed.SimpleSprinkler", "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 - "Default | UpdateKey": "Nexus:76", - "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:76" }, "Siv's Marriage Mod": { "ID": "6266959802", "MapLocalVersions": { "0.0": "1.4" }, - "Default | UpdateKey": "Nexus:366", - "~1.2.2 | Status": "AssumeBroken" // broke in SMAPI 1.9 (has multiple Mod instances) + "Default | UpdateKey": "Nexus:366" }, "Skill Prestige": { "ID": "alphablackwolf.skillPrestige", "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 - "Default | UpdateKey": "Nexus:569", - "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:569" }, "Skill Prestige: Cooking Adapter": { "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:569", - "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:569" }, "Skip Intro": { "ID": "Pathoschild.SkipIntro", "FormerIDs": "SkipIntro", // changed in 1.4 - "Default | UpdateKey": "Nexus:533" + "Default | UpdateKey": "Nexus:533", + "~1.7.2 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Skull Cavern Elevator": { @@ -1479,8 +1444,7 @@ "Solar Eclipse Event": { "ID": "KoihimeNakamura.SolarEclipseEvent", "Default | UpdateKey": "Nexus:897", - "MapLocalVersions": { "1.3-20170917": "1.3" }, - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "MapLocalVersions": { "1.3-20170917": "1.3" } }, "SpaceCore": { @@ -1500,8 +1464,7 @@ "Sprinkles": { "ID": "Platonymous.Sprinkles", - "Default | UpdateKey": "Chucklefish:4592", - "~1.1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Chucklefish:4592" }, "Sprint and Dash": { @@ -1526,8 +1489,7 @@ "StackSplitX": { "ID": "tstaples.StackSplitX", "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 - "Default | UpdateKey": "Nexus:798", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:798" }, "StaminaRegen": { @@ -1553,15 +1515,13 @@ "Stardew Notification": { "ID": "stardewnotification", - "Default | UpdateKey": "GitHub:monopandora/StardewNotification", - "~1.7 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "GitHub:monopandora/StardewNotification" }, "Stardew Symphony": { "ID": "Omegasis.StardewSymphony", "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:425", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "Default | UpdateKey": "Nexus:425" // added in 1.4.1 }, "StarDustCore": { @@ -1613,8 +1573,7 @@ "Tainted Cellar": { "ID": "TaintedCellar", - "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}", // changed in 1.1 - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 + "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}" // changed in 1.1 }, "Tapper Ready": { @@ -1635,13 +1594,13 @@ "The Long Night": { "ID": "Pathoschild.TheLongNight", - "Default | UpdateKey": "Nexus:1369" + "Default | UpdateKey": "Nexus:1369", + "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.3 }, "Three-heart Dance Partner": { "ID": "ThreeHeartDancePartner", - "Default | UpdateKey": "Nexus:500", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:500" }, "TimeFreeze": { @@ -1659,8 +1618,7 @@ "TimeSpeed": { "ID": "cantorsdust.TimeSpeed", "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis - "Default | UpdateKey": "Nexus:169", - "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Nexus:169" }, "To Do List": { @@ -1676,7 +1634,8 @@ "TractorMod": { "ID": "Pathoschild.TractorMod", "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Default | UpdateKey": "Nexus:1401" + "Default | UpdateKey": "Nexus:1401", + "~4.4.1 | Status": "AssumeBroken" // broke in SDV 1.3 }, "TrainerMod": { @@ -1749,8 +1708,7 @@ }, "Wonderful Farm Life": { - "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 + "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}" }, "XmlSerializerRetool": { @@ -1761,15 +1719,13 @@ "Xnb Loader": { "ID": "Entoarox.XnbLoader", - "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature) }, "zDailyIncrease": { "ID": "zdailyincrease", "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest - "Default | UpdateKey": "Chucklefish:4247", - "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "Chucklefish:4247" }, "Zoom Out Extreme": { @@ -1822,8 +1778,7 @@ "Zoryn's Movement Mod": { "ID": "Zoryn.MovementModifier", "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" }, "Zoryn's Regen Mod": { -- cgit From f451e172e2d2cf1959c86fdb24c708ca5b1924f7 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Thu, 19 Apr 2018 01:35:15 -0500 Subject: update documentation format and document field --- .../Framework/Commands/Player/AddCommand.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 453b8e32..803ae7f6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -16,6 +16,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Provides methods for searching and constructing items. private readonly ItemRepository Items = new ItemRepository(); + /// All possible item types along with Name. private readonly string[] ItemTypeAndName = Enum.GetNames(typeof(ItemType)).Union(new string[] { "Name" }).ToArray(); /********* @@ -75,9 +76,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ** Private methods *********/ - /// - /// Finds a matching item by item type and id. - /// + /// Finds a matching item by item type and id. /// Writes messages to the console and log file. /// The command arguments. /// The raw item type. @@ -101,9 +100,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player } } - /// - /// Finds a matching item by name. - /// + /// Finds a matching item by name. /// Writes messages to the console and log file. /// The command arguments. /// The item name. -- cgit From a269141e9a81949860e88710eb0fe0d5281c7eb5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Apr 2018 18:59:27 -0400 Subject: update for Stardew Valley 1.3.0.33 release build (#453) --- src/SMAPI/Framework/SGame.cs | 707 ++++++++++++++++++++----------------------- 1 file changed, 322 insertions(+), 385 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c33aa084..4ec46e5c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -20,6 +20,7 @@ using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; +using xTile.Layers; using SFarmer = StardewValley.Farmer; namespace StardewModdingAPI.Framework @@ -778,119 +779,82 @@ namespace StardewModdingAPI.Framework } this.drawOverlays(Game1.spriteBatch); } - else + else if ((int)Game1.gameMode == 6 || (int)Game1.gameMode == 3 && Game1.currentLocation == null) { - int num1; - switch (Game1.gameMode) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + string str1 = ""; + for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) + str1 += "."; + string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); + string s = str2 + str1; + string str3 = str2 + "... "; + int widthOfString = SpriteText.getWidthOfString(str3); + int height = 64; + int x = 64; + int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; + SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) { - case 3: - num1 = Game1.currentLocation == null ? 1 : 0; - break; - case 6: - num1 = 1; - break; - default: - num1 = 0; - break; + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); } - if (num1 != 0) + this.drawOverlays(Game1.spriteBatch); + //base.Draw(gameTime); + } + else + { + Microsoft.Xna.Framework.Rectangle rectangle; + if ((int)Game1.gameMode == 0) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - string str1 = ""; - for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) - str1 += "."; - string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string s = str2 + str1; - string str3 = str2 + "... "; - int widthOfString = SpriteText.getWidthOfString(str3); - int height = 64; - int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; - SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - this.drawOverlays(Game1.spriteBatch); - //base.Draw(gameTime); } else { - Viewport viewport1; - if ((int)Game1.gameMode == 0) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - } - else + if (Game1.drawLighting) { - Microsoft.Xna.Framework.Rectangle bounds; - if (Game1.drawLighting) + this.GraphicsDevice.SetRenderTarget(Game1.lightmap); + this.GraphicsDevice.Clear(Color.White * 0.0f); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight)); + for (int index = 0; index < Game1.currentLightSources.Count; ++index) { - this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Color.White * 0.0f); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight)); - for (int index = 0; index < Game1.currentLightSources.Count; ++index) - { - if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D lightTexture = Game1.currentLightSources.ElementAt(index).lightTexture; - Vector2 position = Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds); - Color color = (Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color); - double num2 = 0.0; - bounds = Game1.currentLightSources.ElementAt(index).lightTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.currentLightSources.ElementAt(index).lightTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num3 = (double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (double)(Game1.options.lightingQuality / 2); - int num4 = 0; - double num5 = 0.899999976158142; - spriteBatch.Draw(lightTexture, position, sourceRectangle, color, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); - } - } - Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screen); + if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } - if (Game1.bloomDay && Game1.bloom != null) - Game1.bloom.BeginDraw(); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - this.Events.Graphics_OnPreRenderEvent.Raise(); - if (Game1.background != null) - Game1.background.draw(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.currentLocation.drawWater(Game1.spriteBatch); + Game1.spriteBatch.End(); + this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screen); + } + if (Game1.bloomDay && Game1.bloom != null) + Game1.bloom.BeginDraw(); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Graphics_OnPreRenderEvent.Raise(); + if (Game1.background != null) + Game1.background.draw(Game1.spriteBatch); + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.currentLocation.drawWater(Game1.spriteBatch); + IEnumerable source = Game1.currentLocation.farmers; + if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) + source = (IEnumerable)Game1.currentLocation.currentEvent.farmerActors; + IEnumerable farmers = source.Where((Func)(farmer => + { + if (!farmer.IsLocalPlayer) + return !(bool)((NetFieldBase)farmer.hidden); + return true; + })); + if (!Game1.currentLocation.shouldHideCharacters()) + { if (Game1.CurrentEvent == null) { foreach (NPC character in Game1.currentLocation.characters) { - if (!(bool)((NetFieldBase)character.swimming) && !character.HideShadow && !character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num2 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num3 = (4.0 + (double)character.yJumpOffset / 40.0) * (double)(float)((NetFieldBase)character.scale); - int num4 = 0; - double num5 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); - } + if (!(bool)((NetFieldBase)character.swimming) && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); } } else @@ -898,26 +862,10 @@ namespace StardewModdingAPI.Framework foreach (NPC actor in Game1.CurrentEvent.actors) { if (!(bool)((NetFieldBase)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num2 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num3 = (4.0 + (double)actor.yJumpOffset / 40.0) * (double)(float)((NetFieldBase)actor.scale); - int num4 = 0; - double num5 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); - } + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (SFarmer farmer in Game1.currentLocation.getFarmers()) + foreach (SFarmer farmer in farmers) { if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) { @@ -926,22 +874,25 @@ namespace StardewModdingAPI.Framework Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f)); Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); Color white = Color.White; - double num2 = 0.0; - Microsoft.Xna.Framework.Rectangle bounds2 = Game1.shadowTexture.Bounds; - double x = (double)bounds2.Center.X; - bounds2 = Game1.shadowTexture.Bounds; - double y = (double)bounds2.Center.Y; + double num1 = 0.0; + Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; Vector2 origin = new Vector2((float)x, (float)y); - double num3 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); - int num4 = 0; - double num5 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + double num2 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); + int num3 = 0; + double num4 = 0.0; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); } } - Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.mapDisplayDevice.EndScene(); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + } + Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (!Game1.currentLocation.shouldHideCharacters()) + { if (Game1.CurrentEvent == null) { foreach (NPC character in Game1.currentLocation.characters) @@ -958,7 +909,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (SFarmer farmer in Game1.currentLocation.getFarmers()) + foreach (SFarmer farmer in farmers) { if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) { @@ -967,293 +918,279 @@ namespace StardewModdingAPI.Framework Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f)); Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); Color white = Color.White; - double num2 = 0.0; - Microsoft.Xna.Framework.Rectangle bounds2 = Game1.shadowTexture.Bounds; - double x = (double)bounds2.Center.X; - bounds2 = Game1.shadowTexture.Bounds; - double y = (double)bounds2.Center.Y; + double num1 = 0.0; + Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; Vector2 origin = new Vector2((float)x, (float)y); - double num3 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); - int num4 = 0; - double num5 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + double num2 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); + int num3 = 0; + double num4 = 0.0; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); } } - if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0)); - Game1.currentLocation.draw(Game1.spriteBatch); - if (!Game1.eventUp || Game1.currentLocation.currentEvent == null || Game1.currentLocation.currentEvent.messageToScreen == null) - ; - if (Game1.player.ActiveObject == null && ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Name.Equals("Farm")) - this.drawFarmBuildings(); - if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f); - foreach (Warp warp in (NetList>)Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.mapDisplayDevice.EndScene(); - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways") || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))) - Game1.drawPlayerHeldObject(Game1.player); - if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) - { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.mapDisplayDevice.EndScene(); - } - if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + } + if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) + Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); + if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0)); + Game1.currentLocation.draw(Game1.spriteBatch); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; + } + if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Name.Equals("Farm")) + this.drawFarmBuildings(); + if (Game1.tvStation >= 0) + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + if (Game1.panMode) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f); + foreach (Warp warp in (NetList>)Game1.currentLocation.warps) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); + } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.mapDisplayDevice.EndScene(); + Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawPlayerHeldObject(Game1.player); + else if (Game1.displayFarmer && Game1.player.ActiveObject != null) + { + if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) { - Color color = Color.White; - switch ((int)((double)Game1.toolHold / 600.0) + 2) + Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); + rectangle = Game1.player.GetBoundingBox(); + Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); + Size size1 = Game1.viewport.Size; + if (layer1.PickTile(mapDisplayLocation1, size1) != null) { - case 1: - color = Tool.copperColor; - break; - case 2: - color = Tool.steelColor; - break; - case 3: - color = Tool.goldColor; - break; - case 4: - color = Tool.iridiumColor; - break; + Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); + rectangle = Game1.player.GetBoundingBox(); + Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); + Size size2 = Game1.viewport.Size; + if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) + goto label_140; } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); + else + goto label_140; } - if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)) - Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64))))) + Game1.drawPlayerHeldObject(Game1.player); + } +label_140: + if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + { + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.mapDisplayDevice.EndScene(); + } + if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + { + Color color = Color.White; + switch ((int)((double)Game1.toolHold / 600.0) + 2) { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + case 1: + color = Tool.copperColor; + break; + case 2: + color = Tool.steelColor; + break; + case 3: + color = Tool.goldColor; + break; + case 4: + color = Tool.iridiumColor; + break; } - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); + } + if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) + { + foreach (WeatherDebris weatherDebris in Game1.debrisWeather) + weatherDebris.draw(Game1.spriteBatch); + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); + if (Game1.screenGlow) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); + if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) + Game1.player.CurrentTool.draw(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64))))) + { + for (int index = 0; index < Game1.rainDrops.Length; ++index) + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + } + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + foreach (NPC actor in Game1.currentLocation.currentEvent.actors) { - foreach (NPC actor in Game1.currentLocation.currentEvent.actors) + if (actor.isEmoting) { - if (actor.isEmoting) - { - Vector2 localPosition = actor.getLocalPosition(Game1.viewport); - localPosition.Y -= 140f; - if (actor.Age == 2) - localPosition.Y += 32f; - else if (actor.Gender == 1) - localPosition.Y += 10f; - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); - } + Vector2 localPosition = actor.getLocalPosition(Game1.viewport); + localPosition.Y -= 140f; + if (actor.Age == 2) + localPosition.Y += 32f; + else if (actor.Gender == 1) + localPosition.Y += 10f; + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); } } + } + Game1.spriteBatch.End(); + if (Game1.drawLighting) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); + if (Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)) + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); Game1.spriteBatch.End(); - if (Game1.drawLighting) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); - if (Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.drawGrid) - { - int num2 = -Game1.viewport.X % 64; - float num3 = (float)(-Game1.viewport.Y % 64); - int num4 = num2; - while (true) - { - int num5 = num4; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - int width1 = viewport1.Width; - if (num5 < width1) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - int x = num4; - int y = (int)num3; - int width2 = 1; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - int height = viewport1.Height; - Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width2, height); - Color color = Color.Red * 0.5f; - spriteBatch.Draw(staminaRect, destinationRectangle, color); - num4 += 64; - } - else - break; - } - float num6 = num3; - while (true) - { - double num5 = (double)num6; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - double height1 = (double)viewport1.Height; - if (num5 < height1) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - int x = num2; - int y = (int)num6; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - int width = viewport1.Width; - int height2 = 1; - Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, height2); - Color color = Color.Red * 0.5f; - spriteBatch.Draw(staminaRect, destinationRectangle, color); - num6 += 64f; - } - else - break; - } - } - if ((uint)Game1.currentBillboard > 0U) - this.drawBillboard(); - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode) && !Game1.HostPaused) + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.drawGrid) + { + int x1 = -Game1.viewport.X % 64; + float num1 = (float)(-Game1.viewport.Y % 64); + int x2 = x1; + while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) { - this.Events.Graphics_OnPreRenderHudEvent.Raise(); - this.drawHUD(); - this.Events.Graphics_OnPostRenderHudEvent.Raise(); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); + x2 += 64; } - else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); - if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) + float num2 = num1; + while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) { - for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) - Game1.hudMessages[i].draw(Game1.spriteBatch, i); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); + num2 += 64f; } } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) - this.drawDialogueBox(); - if (Game1.progressBar) + if (Game1.currentBillboard != 0) + this.drawBillboard(); + if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Color.DimGray); + this.Events.Graphics_OnPreRenderHudEvent.Raise(); + this.drawHUD(); + this.Events.Graphics_OnPostRenderHudEvent.Raise(); } - if (Game1.eventUp && (Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - if (Game1.isRaining && (Game1.currentLocation != null && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))) + else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) + Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); + if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds; - Color color = Color.Blue * 0.2f; - spriteBatch.Draw(staminaRect, bounds, color); + for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) + Game1.hudMessages[i].draw(Game1.spriteBatch, i); } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds; - Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - else if ((double)Game1.flashAlpha > 0.0) + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) + this.drawDialogueBox(); + if (Game1.progressBar) + { + SpriteBatch spriteBatch1 = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + int x1 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2; + rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea(); + int y1 = rectangle.Bottom - 128; + int dialogueWidth = Game1.dialogueWidth; + int height1 = 32; + Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); + Color lightGray = Color.LightGray; + spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); + SpriteBatch spriteBatch2 = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + int x2 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2; + rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea(); + int y2 = rectangle.Bottom - 128; + int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); + int height2 = 32; + Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); + Color dimGray = Color.DimGray; + spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + } + if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) + Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation != null && ((bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))) + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + else if ((double)Game1.flashAlpha > 0.0) + { + if (Game1.options.screenFlash) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha)); + Game1.flashAlpha -= 0.1f; + } + if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) + this.drawDialogueBox(); + foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) + overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); + if (Game1.debugMode) + { + StringBuilder debugStringBuilder = Game1._debugStringBuilder; + debugStringBuilder.Clear(); + if (Game1.panMode) { - if (Game1.options.screenFlash) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds; - Color color = Color.White * Math.Min(1f, Game1.flashAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - Game1.flashAlpha -= 0.1f; + debugStringBuilder.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64); + debugStringBuilder.Append(","); + debugStringBuilder.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64); } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) - this.drawDialogueBox(); - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); - if (Game1.debugMode) + else { - StringBuilder debugStringBuilder = Game1._debugStringBuilder; - debugStringBuilder.Clear(); - if (Game1.panMode) - { - debugStringBuilder.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64); - debugStringBuilder.Append(","); - debugStringBuilder.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64); - } - else - { - debugStringBuilder.Append("player: "); - debugStringBuilder.Append(Game1.player.getStandingX() / 64); - debugStringBuilder.Append(", "); - debugStringBuilder.Append(Game1.player.getStandingY() / 64); - } - debugStringBuilder.Append(" mouseTransparency: "); - debugStringBuilder.Append(Game1.mouseCursorTransparency); - debugStringBuilder.Append(" mousePosition: "); - debugStringBuilder.Append(Game1.getMouseX()); - debugStringBuilder.Append(","); - debugStringBuilder.Append(Game1.getMouseY()); - debugStringBuilder.Append(Environment.NewLine); - debugStringBuilder.Append("debugOutput: "); - debugStringBuilder.Append(Game1.debugOutput); - Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + debugStringBuilder.Append("player: "); + debugStringBuilder.Append(Game1.player.getStandingX() / 64); + debugStringBuilder.Append(", "); + debugStringBuilder.Append(Game1.player.getStandingY() / 64); } - if (Game1.showKeyHelp) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null) + debugStringBuilder.Append(" mouseTransparency: "); + debugStringBuilder.Append(Game1.mouseCursorTransparency); + debugStringBuilder.Append(" mousePosition: "); + debugStringBuilder.Append(Game1.getMouseX()); + debugStringBuilder.Append(","); + debugStringBuilder.Append(Game1.getMouseY()); + debugStringBuilder.Append(Environment.NewLine); + debugStringBuilder.Append("debugOutput: "); + debugStringBuilder.Append(Game1.debugOutput); + Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + } + if (Game1.showKeyHelp) + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + if (Game1.activeClickableMenu != null) + { + try { - try - { - this.Events.Graphics_OnPreRenderGuiEvent.Raise(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - this.Events.Graphics_OnPostRenderGuiEvent.Raise(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } - else if (Game1.farmEvent != null) - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - if (Game1.HostPaused) + catch (Exception ex) { - string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); - SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1); + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); } - this.RaisePostRender(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - this.renderScreenBuffer(); - //base.Draw(gameTime); } + else if (Game1.farmEvent != null) + Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + if (Game1.HostPaused) + { + string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); + SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1); + } + this.RaisePostRender(); + Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); + this.renderScreenBuffer(); + //base.Draw(gameTime); } } } -- cgit From 1827e94fa221cbea4f075f4557a030cc4d37a0d5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Apr 2018 18:59:37 -0400 Subject: update compatibility list (#453) --- src/SMAPI/StardewModdingAPI.metadata.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 07a63a20..f7a30537 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -502,7 +502,7 @@ "Daily News": { "ID": "bashNinja.DailyNews", "Default | UpdateKey": "Nexus:1141", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~1.2 | Status": "AssumeBroken" // broke in Stardew Valley 1.3 (or depends on CustomTV which broke) }, "Daily Quest Anywhere": { @@ -618,7 +618,7 @@ "ID": "Omegasis.Fall28SnowDay", "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:486", // added in 1.4.1 - "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything }, "Farm Automation: Barn Door Automation": { @@ -998,7 +998,8 @@ "Move Faster": { "ID": "shuaiz.MoveFasterMod", - "Default | UpdateKey": "Nexus:1351" + "Default | UpdateKey": "Nexus:1351", + "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) }, "Multiple Sprites and Portraits On Rotation (File Loading)": { -- cgit From d06e7d147afae95464935a24cfd6d0bf76fce7b6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Apr 2018 19:03:49 -0400 Subject: fix SMAPI build intermittently failing due to undeclared dependency on analyzers project --- src/SMAPI.sln | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 56898a32..d84ce589 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -6,6 +6,9 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\StardewModdingAPI.Mods.ConsoleCommands.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" + ProjectSection(ProjectDependencies) = postProject + {80AD8528-AA49-4731-B4A6-C691845815A1} = {80AD8528-AA49-4731-B4A6-C691845815A1} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject -- cgit From 1b527f0b2573ba54024cdee80680983030246f4f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Apr 2018 19:11:50 -0400 Subject: detect broken references to methods which changed generic return type (#453) Previously generic types couldn't be compared correctly, since we'd end up with false differences like "Dictionary`1 != Dictionary". That seems to be fixed now, possibly due to the PDB file being included. --- .../ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 2cbb3a8e..ecad649a 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -78,10 +78,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodReference methodReference = RewriteHelper.AsMethodReference(instruction); if (methodReference != null && this.ShouldValidate(methodReference.DeclaringType)) { - // can't compare generic type parameters between definition and reference - if (methodReference.ReturnType.IsGenericInstance || methodReference.ReturnType.IsGenericParameter) - return InstructionHandleResult.None; - // get potential targets MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray(); if (candidateMethods == null || !candidateMethods.Any()) -- cgit From b346d28d3858b79c6c4cde55faac34ecdedeaff1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Apr 2018 20:35:16 -0400 Subject: fix GetApi interface validation errors not naming interface --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index cde7847e..d0b6d332 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * Dropped some deprecated APIs. * Fixed assets loaded by temporary content managers not being editable. * Fixed issue where assets didn't reload correctly when the player switches language. + * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * For SMAPI developers: * Added more consistent crossplatform handling using a new `EnvironmentUtility`. diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index e579a830..008a80f5 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -82,12 +82,12 @@ namespace StardewModdingAPI.Framework.ModHelpers } if (!typeof(TInterface).IsInterface) { - this.Monitor.Log("Tried to map a mod-provided API to a class; must be a public interface.", LogLevel.Error); + this.Monitor.Log($"Tried to map a mod-provided API to class '{typeof(TInterface).FullName}'; must be a public interface.", LogLevel.Error); return null; } if (!typeof(TInterface).IsPublic) { - this.Monitor.Log("Tried to map a mod-provided API to a non-public interface; must be a public interface.", LogLevel.Error); + this.Monitor.Log($"Tried to map a mod-provided API to non-public interface '{typeof(TInterface).FullName}'; must be a public interface.", LogLevel.Error); return null; } -- cgit From eead352af26d0fcc5cac147d0eb5ec384854d931 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Apr 2018 20:37:17 -0400 Subject: rewrite world/player state tracking (#453) --- docs/release-notes.md | 6 +- src/SMAPI/Framework/SGame.cs | 279 +++++++++------------ .../StateTracking/Comparers/EquatableComparer.cs | 32 +++ .../Comparers/ObjectReferenceComparer.cs | 29 +++ .../FieldWatchers/BaseDisposableWatcher.cs | 36 +++ .../FieldWatchers/ComparableWatcher.cs | 62 +++++ .../FieldWatchers/NetDictionaryWatcher.cs | 103 ++++++++ .../StateTracking/FieldWatchers/NetValueWatcher.cs | 83 ++++++ .../FieldWatchers/ObservableCollectionWatcher.cs | 86 +++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 54 ++++ .../Framework/StateTracking/ICollectionWatcher.cs | 17 ++ .../Framework/StateTracking/IDictionaryWatcher.cs | 7 + src/SMAPI/Framework/StateTracking/IValueWatcher.cs | 15 ++ src/SMAPI/Framework/StateTracking/IWatcher.cs | 24 ++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 202 +++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 13 + 16 files changed, 884 insertions(+), 164 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs create mode 100644 src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IValueWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/PlayerTracker.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index d0b6d332..73fe0710 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,4 @@ # Release notes - + * Overhauled world/player state tracking: + * much more efficient than previous method; + * uses net field events where available; + * lays groundwork for tracking events for multiple players. ## 2.5.5 * For players: diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4ec46e5c..cea86dfb 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,6 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -13,6 +13,8 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.StateTracking; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -22,6 +24,7 @@ using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; using SFarmer = StardewValley.Farmer; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -60,6 +63,9 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; + /// Whether the after-load events were raised for this session. + private bool RaisedAfterLoadEvent; + /// Whether the game is returning to the menu. private bool IsExitingToTitle; @@ -75,50 +81,26 @@ namespace StardewModdingAPI.Framework /// The player input as of the previous tick. private InputState PreviousInput = new InputState(); - /// The window size value at last check. - private Point PreviousWindowSize; - - /// The save ID at last check. - private ulong PreviousSaveID; - - /// A hash of at last check. - private int PreviousGameLocations; - - /// A hash of the current location's at last check. - private int PreviousLocationObjects; - - /// The player's inventory at last check. - private IDictionary PreviousItems; - - /// The player's combat skill level at last check. - private int PreviousCombatLevel; - - /// The player's farming skill level at last check. - private int PreviousFarmingLevel; - - /// The player's fishing skill level at last check. - private int PreviousFishingLevel; - - /// The player's foraging skill level at last check. - private int PreviousForagingLevel; + /// The underlying watchers for convenience. These are accessible individually as separate properties. + private readonly List Watchers = new List(); - /// The player's mining skill level at last check. - private int PreviousMiningLevel; + /// Tracks changes to the window size. + private readonly IValueWatcher WindowSizeWatcher; - /// The player's luck skill level at last check. - private int PreviousLuckLevel; + /// Tracks changes to the current player. + private PlayerTracker CurrentPlayerTracker; - /// The player's location at last check. - private GameLocation PreviousGameLocation; + /// Tracks changes to the time of day (in 24-hour military format). + private readonly IValueWatcher TimeWatcher; - /// The active game menu at last check. - private IClickableMenu PreviousActiveMenu; + /// Tracks changes to the save ID. + private readonly IValueWatcher SaveIdWatcher; - /// The mine level at last check. - private int PreviousMineLevel; + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationsWatcher; - /// The time of day (in 24-hour military format) at last check. - private int PreviousTime; + /// Tracks changes to . + private readonly IValueWatcher ActiveMenuWatcher; /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -156,7 +138,10 @@ namespace StardewModdingAPI.Framework /// A callback to invoke after the game finishes initialising. internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised) { - // initialise + // init XNA + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + + // init SMAPI this.Monitor = monitor; this.Events = eventManager; this.FirstUpdate = true; @@ -165,8 +150,21 @@ namespace StardewModdingAPI.Framework if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); - // set XNA option required by Stardew Valley - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + // init watchers + Game1.locations = new ObservableCollection(); + this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); + this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); + this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); + this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); + this.LocationsWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); + this.Watchers.AddRange(new IWatcher[] + { + this.SaveIdWatcher, + this.WindowSizeWatcher, + this.TimeWatcher, + this.ActiveMenuWatcher, + this.LocationsWatcher + }); } /**** @@ -203,31 +201,58 @@ namespace StardewModdingAPI.Framework return; } - // While a background new-day task is in progress, the game skips its own update logic - // and defers to the XNA Update method. Running mod code in parallel to the background - // update is risky, because data changes can conflict (e.g. collection changed during - // enumeration errors) and data may change unexpectedly from one mod instruction to the - // next. + // While a background task is in progress, the game may make changes to the game + // state while mods are running their code. This is risky, because data changes can + // conflict (e.g. collection changed during enumeration errors) and data may change + // unexpectedly from one mod instruction to the next. // // Therefore we can just run Game1.Update here without raising any SMAPI events. There's // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one // update tick are neglible and not worth the complications of bypassing Game1.Update. - if (Game1._newDayTask != null) + if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } - // game is asynchronously loading a save, block mod events to avoid conflicts - if (Game1.gameMode == Game1.loadingMode) + /********* + ** Update context + *********/ + if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) { - base.Update(gameTime); - this.Events.Specialised_UnvalidatedUpdateTick.Raise(); - return; + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) + this.AfterLoadTimer--; + Context.IsWorldReady = this.AfterLoadTimer <= 0; } + /********* + ** Update watchers + *********/ + // reset player + if (Context.IsWorldReady) + { + if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) + { + this.CurrentPlayerTracker?.Dispose(); + this.CurrentPlayerTracker = new PlayerTracker(Game1.player); + } + } + else + { + if (this.CurrentPlayerTracker != null) + { + this.CurrentPlayerTracker.Dispose(); + this.CurrentPlayerTracker = null; + } + } + + // update values + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + this.CurrentPlayerTracker?.Update(); + /********* ** Save events + suppress events during save *********/ @@ -300,19 +325,12 @@ namespace StardewModdingAPI.Framework /********* ** After load events *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) + if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) { - if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) - this.AfterLoadTimer--; - - if (this.AfterLoadTimer == 0) - { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - Context.IsWorldReady = true; - - this.Events.Save_AfterLoad.Raise(); - this.Events.Time_AfterDayStarted.Raise(); - } + this.RaisedAfterLoadEvent = true; + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.Save_AfterLoad.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } /********* @@ -339,11 +357,10 @@ namespace StardewModdingAPI.Framework // event because we need to notify mods after the game handles the resize, so the // game's metadata (like Game1.viewport) are updated. That's a bit complicated // since the game adds & removes its own handler on the fly. - if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) + if (this.WindowSizeWatcher.IsChanged) { - Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); this.Events.Graphics_Resize.Raise(); - this.PreviousWindowSize = size; + this.WindowSizeWatcher.Reset(); } /********* @@ -431,10 +448,11 @@ namespace StardewModdingAPI.Framework /********* ** Menu events *********/ - if (Game1.activeClickableMenu != this.PreviousActiveMenu) + if (this.ActiveMenuWatcher.IsChanged) { - IClickableMenu previousMenu = this.PreviousActiveMenu; - IClickableMenu newMenu = Game1.activeClickableMenu; + IClickableMenu previousMenu = this.ActiveMenuWatcher.PreviousValue; + IClickableMenu newMenu = this.ActiveMenuWatcher.CurrentValue; + this.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards // log context if (this.VerboseLogging) @@ -452,10 +470,6 @@ namespace StardewModdingAPI.Framework this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); else this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu)); - - // update previous menu - // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) - this.PreviousActiveMenu = newMenu; } /********* @@ -463,70 +477,56 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { + // update player info + PlayerTracker curPlayer = this.CurrentPlayerTracker; + // raise current location changed - // ReSharper disable once PossibleUnintendedReferenceComparison - if (Game1.currentLocation != this.PreviousGameLocation) + if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) { if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(this.PreviousGameLocation, Game1.currentLocation)); + this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(curPlayer.LocationWatcher.PreviousValue, newLocation)); } // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + if (this.LocationsWatcher.IsChanged) this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); // raise events that shouldn't be triggered on initial load - if (Game1.uniqueIDForThisGame == this.PreviousSaveID) + if (!this.SaveIdWatcher.IsChanged) { // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel)); - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel)); - if (Game1.player.fishingLevel != this.PreviousFishingLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel)); - if (Game1.player.foragingLevel != this.PreviousForagingLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel)); - if (Game1.player.miningLevel != this.PreviousMiningLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel)); - if (Game1.player.luckLevel != this.PreviousLuckLevel) - this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); + foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); // raise player inventory changed - ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.Items, this.PreviousItems).ToArray(); + ItemStackChange[] changedItems = curPlayer.GetInventoryChanges().ToArray(); if (changedItems.Any()) this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); // raise current location's object list changed - if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects.FieldDict)); + if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); // raise time changed - if (Game1.timeOfDay != this.PreviousTime) - this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.PreviousTime, Game1.timeOfDay)); + if (this.TimeWatcher.IsChanged) + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.TimeWatcher.PreviousValue, this.TimeWatcher.CurrentValue)); // raise mine level changed - if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(this.PreviousMineLevel, Game1.mine.mineLevel)); + if (curPlayer.TryGetNewMineLevel(out int mineLevel)) + { + this.Monitor.Log("curPlayer mine level changed", LogLevel.Alert); + this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); + } } - - // update state - this.PreviousGameLocations = this.GetHash(Game1.locations); - this.PreviousGameLocation = Game1.currentLocation; - this.PreviousCombatLevel = Game1.player.combatLevel; - this.PreviousFarmingLevel = Game1.player.farmingLevel; - this.PreviousFishingLevel = Game1.player.fishingLevel; - this.PreviousForagingLevel = Game1.player.foragingLevel; - this.PreviousMiningLevel = Game1.player.miningLevel; - this.PreviousLuckLevel = Game1.player.luckLevel; - this.PreviousItems = Game1.player.Items.Where(n => n != null).Distinct().ToDictionary(n => n, n => n.Stack); - this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); - this.PreviousTime = Game1.timeOfDay; - this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; - this.PreviousSaveID = Game1.uniqueIDForThisGame; } + // update state + this.CurrentPlayerTracker?.Reset(); + this.LocationsWatcher.Reset(); + this.SaveIdWatcher.Reset(); + this.TimeWatcher.Reset(); + /********* ** Game update *********/ @@ -982,7 +982,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } -label_140: + label_140: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1204,52 +1204,7 @@ label_140: { Context.IsWorldReady = false; this.AfterLoadTimer = 5; - this.PreviousSaveID = 0; - } - - - - /// Get the player inventory changes between two states. - /// The player's current inventory. - /// The player's previous inventory. - private IEnumerable GetInventoryChanges(IEnumerable current, IDictionary previous) - { - current = current.Where(n => n != null).ToArray(); - foreach (Item item in current) - { - // stack size changed - if (previous != null && previous.ContainsKey(item)) - { - if (previous[item] != item.Stack) - yield return new ItemStackChange { Item = item, StackChange = item.Stack - previous[item], ChangeType = ChangeType.StackChange }; - } - - // new item - else - yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; - } - - // removed items - if (previous != null) - { - foreach (var entry in previous) - { - if (current.Any(i => i == entry.Key)) - continue; - - yield return new ItemStackChange { Item = entry.Key, StackChange = -entry.Key.Stack, ChangeType = ChangeType.Removed }; - } - } - } - - /// Get a hash value for an enumeration. - /// The enumeration of items to hash. - private int GetHash(IEnumerable enumerable) - { - int hash = 0; - foreach (object v in enumerable) - hash ^= v.GetHashCode(); - return hash; + this.RaisedAfterLoadEvent = false; } /// Raise the if there are any listeners. diff --git a/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs new file mode 100644 index 00000000..a96ffdb6 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// Compares instances using . + /// The value type. + internal class EquatableComparer : IEqualityComparer where T : IEquatable + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + if (x == null) + return y == null; + return x.Equals(y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs new file mode 100644 index 00000000..ef9adafb --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// A comparer which considers two references equal if they point to the same instance. + /// The value type. + internal class ObjectReferenceComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + return object.ReferenceEquals(x, y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs new file mode 100644 index 00000000..40ec6c57 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// The base implementation for a disposable watcher. + internal abstract class BaseDisposableWatcher : IDisposable + { + /********* + ** Properties + *********/ + /// Whether the watcher has been disposed. + protected bool IsDisposed { get; private set; } + + + /********* + ** Public methods + *********/ + /// Stop watching the field and release all references. + public virtual void Dispose() + { + this.IsDisposed = true; + } + + + /********* + ** Protected methods + *********/ + /// Throw an exception if the watcher is disposed. + /// The watcher is disposed. + protected void AssertNotDisposed() + { + if (this.IsDisposed) + throw new ObjectDisposedException(this.GetType().Name); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs new file mode 100644 index 00000000..d51fc2ac --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a value using a specified instance. + internal class ComparableWatcher : IValueWatcher + { + /********* + ** Properties + *********/ + /// Get the current value. + private readonly Func GetValue; + + /// The equality comparer. + private readonly IEqualityComparer Comparer; + + + /********* + ** Accessors + *********/ + /// The field value at the last reset. + public T PreviousValue { get; private set; } + + /// The latest value. + public T CurrentValue { get; private set; } + + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Get the current value. + /// The equality comparer which indicates whether two values are the same. + public ComparableWatcher(Func getValue, IEqualityComparer comparer) + { + this.GetValue = getValue; + this.Comparer = comparer; + this.PreviousValue = getValue(); + } + + /// Update the current value if needed. + public void Update() + { + this.CurrentValue = this.GetValue(); + this.IsChanged = !this.Comparer.Equals(this.PreviousValue, this.CurrentValue); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Release any references if needed when the field is no longer needed. + public void Dispose() { } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs new file mode 100644 index 00000000..7a2bf84e --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net dictionary field. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + internal class NetDictionaryWatcher : BaseDisposableWatcher, IDictionaryWatcher + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + /********* + ** Properties + *********/ + /// The pairs added since the last reset. + private readonly IDictionary PairsAdded = new Dictionary(); + + /// The pairs demoved since the last reset. + private readonly IDictionary PairsRemoved = new Dictionary(); + + /// The field being watched. + private readonly NetDictionary Field; + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.PairsAdded.Count > 0 || this.PairsRemoved.Count > 0; + + /// The values added since the last reset. + public IEnumerable> Added => this.PairsAdded; + + /// The values removed since the last reset. + public IEnumerable> Removed => this.PairsRemoved; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetDictionaryWatcher(NetDictionary field) + { + this.Field = field; + + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PairsAdded.Clear(); + this.PairsRemoved.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the dictionary. + /// The entry key. + /// The entry value. + private void OnValueAdded(TKey key, TValue value) + { + this.PairsAdded[key] = value; + } + + /// A callback invoked when an entry is removed from the dictionary. + /// The entry key. + /// The entry value. + private void OnValueRemoved(TKey key, TValue value) + { + if (!this.PairsRemoved.ContainsKey(key)) + this.PairsRemoved[key] = value; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs new file mode 100644 index 00000000..188ed9f3 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs @@ -0,0 +1,83 @@ +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net value field. + internal class NetValueWatcher : BaseDisposableWatcher, IValueWatcher where TSelf : NetFieldBase + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetFieldBase Field; + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + /// The field value at the last reset. + public T PreviousValue { get; private set; } + + /// The latest value. + public T CurrentValue { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetValueWatcher(NetFieldBase field) + { + this.Field = field; + this.PreviousValue = field.Value; + this.CurrentValue = field.Value; + + field.fieldChangeVisibleEvent += this.OnValueChanged; + field.fieldChangeEvent += this.OnValueChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.fieldChangeEvent -= this.OnValueChanged; + this.Field.fieldChangeVisibleEvent -= this.OnValueChanged; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when the field's value changes. + /// The field being watched. + /// The old field value. + /// The new field value. + private void OnValueChanged(TSelf field, T oldValue, T newValue) + { + this.CurrentValue = newValue; + this.IsChanged = true; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs new file mode 100644 index 00000000..34a97097 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to an observable collection. + internal class ObservableCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly ObservableCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public ObservableCollectionWatcher(ObservableCollection field) + { + this.Field = field; + field.CollectionChanged += this.OnCollectionChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + this.Field.CollectionChanged -= this.OnCollectionChanged; + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added or removed from the collection. + /// The event sender. + /// The event arguments. + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null) + this.AddedImpl.AddRange(e.NewItems.Cast()); + if (e.OldItems != null) + this.RemovedImpl.AddRange(e.OldItems.Cast()); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs new file mode 100644 index 00000000..bf261bb5 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Netcode; +using StardewModdingAPI.Framework.StateTracking.Comparers; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// Provides convenience wrappers for creating watchers. + internal static class WatcherFactory + { + /********* + ** Public methods + *********/ + /// Get a watcher for an value. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForEquatable(Func getValue) where T : IEquatable + { + return new ComparableWatcher(getValue, new EquatableComparer()); + } + + /// Get a watcher which detects when an object reference changes. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForReference(Func getValue) + { + return new ComparableWatcher(getValue, new ObjectReferenceComparer()); + } + + /// Get a watcher for an observable collection. + /// The value type. + /// The observable collection. + public static ObservableCollectionWatcher ForObservableCollection(ObservableCollection collection) + { + return new ObservableCollectionWatcher(collection); + } + + /// Get a watcher for a net dictionary. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + /// The net field. + public static NetDictionaryWatcher ForNetDictionary(NetDictionary field) + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + return new NetDictionaryWatcher(field); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs new file mode 100644 index 00000000..7a7759e3 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a collection. + internal interface ICollectionWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The values added since the last reset. + IEnumerable Added { get; } + + /// The values removed since the last reset. + IEnumerable Removed { get; } + } +} diff --git a/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs new file mode 100644 index 00000000..691ed377 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a dictionary. + internal interface IDictionaryWatcher : ICollectionWatcher> { } +} diff --git a/src/SMAPI/Framework/StateTracking/IValueWatcher.cs b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs new file mode 100644 index 00000000..4afca972 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a value. + internal interface IValueWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The field value at the last reset. + T PreviousValue { get; } + + /// The latest value. + T CurrentValue { get; } + } +} diff --git a/src/SMAPI/Framework/StateTracking/IWatcher.cs b/src/SMAPI/Framework/StateTracking/IWatcher.cs new file mode 100644 index 00000000..8c7fa51c --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IWatcher.cs @@ -0,0 +1,24 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which detects changes to something. + internal interface IWatcher : IDisposable + { + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + bool IsChanged { get; } + + + /********* + ** Methods + *********/ + /// Update the current value if needed. + void Update(); + + /// Set the current value as the baseline. + void Reset(); + } +} diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs new file mode 100644 index 00000000..81e074ec --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Locations; +using SFarmer = StardewValley.Farmer; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a player's data. + internal class PlayerTracker : IDisposable + { + /********* + ** Properties + *********/ + /// The player's inventory as of the last reset. + private IDictionary PreviousInventory; + + /// The player's inventory change as of the last update. + private IDictionary CurrentInventory; + + /// The player's last valid location. + private GameLocation LastValidLocation; + + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// The player being tracked. + public SFarmer Player { get; } + + /// The player's current location. + public IValueWatcher LocationWatcher { get; } + + /// Tracks changes to the player's current location's objects. + public IDictionaryWatcher LocationObjectsWatcher { get; private set; } + + /// The player's current mine level. + public IValueWatcher MineLevelWatcher { get; } + + /// Tracks changes to the player's skill levels. + public IDictionary> SkillWatchers { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to track. + public PlayerTracker(SFarmer player) + { + // init player data + this.Player = player; + this.PreviousInventory = this.GetInventory(); + + // init trackers + this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); + this.SkillWatchers = new Dictionary> + { + [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForEquatable(() => player.combatLevel), + [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForEquatable(() => player.farmingLevel), + [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForEquatable(() => player.fishingLevel), + [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForEquatable(() => player.foragingLevel), + [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForEquatable(() => player.luckLevel), + [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForEquatable(() => player.miningLevel) + }; + + // track watchers for convenience + this.Watchers.AddRange(new IWatcher[] + { + this.LocationWatcher, + this.LocationObjectsWatcher, + this.MineLevelWatcher + }); + this.Watchers.AddRange(this.SkillWatchers.Values); + } + + /// Update the current values if needed. + public void Update() + { + // update valid location + this.LastValidLocation = this.GetCurrentLocation(); + + // update watchers + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + + // replace location objects watcher + if (this.LocationWatcher.IsChanged) + { + this.Watchers.Remove(this.LocationObjectsWatcher); + this.LocationObjectsWatcher.Dispose(); + + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.Watchers.Add(this.LocationObjectsWatcher); + } + + // update inventory + this.CurrentInventory = this.GetInventory(); + } + + /// Reset all trackers so their current values are the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + + this.PreviousInventory = this.CurrentInventory; + } + + /// Get the player's current location, ignoring temporary null values. + /// The game will set to null in some cases, e.g. when they're a secondary player in multiplayer and transition to a location that hasn't been synced yet. While that's happening, this returns the player's last valid location instead. + public GameLocation GetCurrentLocation() + { + return this.Player.currentLocation ?? this.LastValidLocation; + } + + /// Get the player inventory changes between two states. + public IEnumerable GetInventoryChanges() + { + IDictionary previous = this.PreviousInventory; + IDictionary current = this.GetInventory(); + foreach (Item item in previous.Keys.Union(current.Keys)) + { + if (!previous.TryGetValue(item, out int prevStack)) + yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; + else if (!current.TryGetValue(item, out int newStack)) + yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed }; + else if (prevStack != newStack) + yield return new ItemStackChange { Item = item, StackChange = newStack - prevStack, ChangeType = ChangeType.StackChange }; + } + } + + /// Get the player skill levels which changed. + public IEnumerable>> GetChangedSkills() + { + return this.SkillWatchers.Where(p => p.Value.IsChanged); + } + + /// Get the player's new location if it changed. + /// The player's current location. + /// Returns whether it changed. + public bool TryGetNewLocation(out GameLocation location) + { + location = this.LocationWatcher.CurrentValue; + return this.LocationWatcher.IsChanged; + } + + /// Get object changes to the player's current location if they there as of the last reset. + /// The object change watcher. + /// Returns whether it changed. + public bool TryGetLocationChanges(out IDictionaryWatcher watcher) + { + if (this.LocationWatcher.IsChanged) + { + watcher = null; + return false; + } + + watcher = this.LocationObjectsWatcher; + return watcher.IsChanged; + } + + /// Get the player's new mine level if it changed. + /// The player's current mine level. + /// Returns whether it changed. + public bool TryGetNewMineLevel(out int mineLevel) + { + mineLevel = this.MineLevelWatcher.CurrentValue; + return this.MineLevelWatcher.IsChanged; + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get the player's current inventory. + private IDictionary GetInventory() + { + return this.Player.Items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9b4a496e..5fe3e32c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -131,6 +131,19 @@ + + + + + + + + + + + + + -- cgit From 4fe7442905aec6efcae940fc22a6d66befb01f74 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Apr 2018 20:38:32 -0400 Subject: add more state tracking logs in verbose mode (#453) --- src/SMAPI/Framework/SGame.cs | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index cea86dfb..b383fcce 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -359,6 +359,8 @@ namespace StardewModdingAPI.Framework // since the game adds & removes its own handler on the fly. if (this.WindowSizeWatcher.IsChanged) { + if (this.VerboseLogging) + this.Monitor.Log($"Context: window size changed to {this.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); this.Events.Graphics_Resize.Raise(); this.WindowSizeWatcher.Reset(); } @@ -454,16 +456,8 @@ namespace StardewModdingAPI.Framework IClickableMenu newMenu = this.ActiveMenuWatcher.CurrentValue; this.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards - // log context if (this.VerboseLogging) - { - if (previousMenu == null) - this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); - else if (newMenu == null) - this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); - else - this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); - } + this.Monitor.Log($"Context: menu changed from {previousMenu?.GetType().FullName ?? "none"} to {newMenu?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events if (newMenu != null) @@ -490,32 +484,59 @@ namespace StardewModdingAPI.Framework // raise location list changed if (this.LocationsWatcher.IsChanged) + { + if (this.VerboseLogging) + { + string added = this.LocationsWatcher.Added.Any() ? string.Join(", ", this.LocationsWatcher.Added.Select(p => p.Name)) : "none"; + string removed = this.LocationsWatcher.Removed.Any() ? string.Join(", ", this.LocationsWatcher.Removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {added}; removed {removed}).", LogLevel.Trace); + } + this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); + } // raise events that shouldn't be triggered on initial load if (!this.SaveIdWatcher.IsChanged) { // raise player leveled up a skill foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); + } // raise player inventory changed ItemStackChange[] changedItems = curPlayer.GetInventoryChanges().ToArray(); if (changedItems.Any()) + { + if (this.VerboseLogging) + this.Monitor.Log("Context: player inventory changed.", LogLevel.Trace); this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); + } // raise current location's object list changed if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: current location objects changed.", LogLevel.Trace); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); + } // raise time changed if (this.TimeWatcher.IsChanged) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: time changed from {this.TimeWatcher.PreviousValue} to {this.TimeWatcher.CurrentValue}.", LogLevel.Trace); this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.TimeWatcher.PreviousValue, this.TimeWatcher.CurrentValue)); + } // raise mine level changed if (curPlayer.TryGetNewMineLevel(out int mineLevel)) { - this.Monitor.Log("curPlayer mine level changed", LogLevel.Alert); + if (this.VerboseLogging) + this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); } } -- cgit From 6113482bef78381c1ca0f176b4f031b7e90d4db0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Apr 2018 21:00:43 -0400 Subject: fix is-asset-cached check not accounting for different behavior in English (#453) --- src/SMAPI/Framework/ContentCore.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index ef001f79..6e915f28 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -460,9 +460,13 @@ namespace StardewModdingAPI.Framework /// The normalised asset name. private bool IsNormalisedKeyLoaded(string normalisedAssetName) { + // default English + if (this.Language == LocalizedContentManager.LanguageCode.en) + return this.Cache.ContainsKey(normalisedAssetName); + + // translated if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) return false; - return localisable ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.Content.GetCurrentLanguage())}") : this.Cache.ContainsKey(normalisedAssetName); -- cgit From 8e9b3741734e32a559b79326f850585a0132e08d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 13:40:24 -0400 Subject: fix crash when closing game window in multiplayer mode (#453) --- src/SMAPI/Framework/SGame.cs | 17 ++++++++++++++++- src/SMAPI/Program.cs | 14 +++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b383fcce..b983de49 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -114,6 +114,9 @@ namespace StardewModdingAPI.Framework /// A callback to invoke after the game finishes initialising. private readonly Action OnGameInitialised; + /// A callback to invoke when the game exits. + private readonly Action OnGameExiting; + /// Simplifies access to private game code. private readonly Reflector Reflection; @@ -136,7 +139,8 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. /// Manages SMAPI events for mods. /// A callback to invoke after the game finishes initialising. - internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised) + /// A callback to invoke when the game exits. + internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -147,6 +151,7 @@ namespace StardewModdingAPI.Framework this.FirstUpdate = true; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; + this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); @@ -167,6 +172,16 @@ namespace StardewModdingAPI.Framework }); } + /// Perform cleanup logic when the game exits. + /// The event sender. + /// The event args. + /// This overrides the logic in and to let SMAPI clean up before exit. + protected override void OnExiting(object sender, EventArgs args) + { + Game1.multiplayer.Disconnect(); + this.OnGameExiting?.Invoke(); + } + /**** ** Intercepted methods & events ****/ diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index cf1c082a..8f91c32b 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -30,6 +31,7 @@ using StardewModdingAPI.Framework.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; +using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI { @@ -197,7 +199,7 @@ namespace StardewModdingAPI // override game SGame.MonitorDuringInitialisation = this.Monitor; SGame.ReflectorDuringInitialisation = this.Reflection; - this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart); + this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -221,10 +223,6 @@ namespace StardewModdingAPI }).Start(); // hook into game events -#if SMAPI_FOR_WINDOWS - ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); -#endif - this.GameInstance.Exiting += (sender, e) => this.Dispose(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles @@ -271,12 +269,11 @@ namespace StardewModdingAPI /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Monitor.Log("Disposing...", LogLevel.Trace); - // skip if already disposed if (this.IsDisposed) return; this.IsDisposed = true; + this.Monitor.Log("Disposing...", LogLevel.Trace); // dispose mod data foreach (IModMetadata mod in this.ModRegistry.GetAll()) @@ -298,6 +295,9 @@ namespace StardewModdingAPI this.CancellationTokenSource?.Dispose(); this.GameInstance?.Dispose(); this.LogFile?.Dispose(); + + // end game (moved from Game1.OnExiting to let us clean up first) + Process.GetCurrentProcess().Kill(); } -- cgit From 902814d308289f169750a615ae573edc348893d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 15:05:08 -0400 Subject: don't send chatbox input to mods (#453) --- docs/release-notes.md | 3 +- src/SMAPI/Framework/Input/InputState.cs | 7 +++ src/SMAPI/Framework/SGame.cs | 107 +++++++++++++++++--------------- 3 files changed, 65 insertions(+), 52 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 73fe0710..ee2d0aa1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,10 +10,11 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. * Added warning when a mod doesn't have update keys (currently only shown in developer mode). - * Dropped some deprecated APIs. * Fixed assets loaded by temporary content managers not being editable. * Fixed issue where assets didn't reload correctly when the player switches language. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. + * **Breaking change**: dropped some deprecated APIs. + * **Breaking change**: mods can't intercept chatbox input, including the game's hotkey to toggle the chatbox (default `T`). * For SMAPI developers: * Added more consistent crossplatform handling using a new `EnvironmentUtility`. diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs index 8b0108ae..7c8676e9 100644 --- a/src/SMAPI/Framework/Input/InputState.cs +++ b/src/SMAPI/Framework/Input/InputState.cs @@ -72,6 +72,13 @@ namespace StardewModdingAPI.Framework.Input return this.GetStatus(button).IsDown(); } + /// Get whether any of the given buttons were pressed or held. + /// The buttons to check. + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + /// Get the current input state. /// The previous input state. public static InputState GetState(InputState previousState) diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b983de49..35e027d8 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -175,7 +175,7 @@ namespace StardewModdingAPI.Framework /// Perform cleanup logic when the game exits. /// The event sender. /// The event args. - /// This overrides the logic in and to let SMAPI clean up before exit. + /// This overrides the logic in to let SMAPI clean up before exit. protected override void OnExiting(object sender, EventArgs args) { Game1.multiplayer.Disconnect(); @@ -396,67 +396,72 @@ namespace StardewModdingAPI.Framework inputState = this.PreviousInput; } - // get cursor position - ICursorPosition cursor; + // raise events + bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); + if (!isChatInput) { - // cursor position - Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); - } - - // raise input events - foreach (var pair in inputState.ActiveButtons) - { - SButton button = pair.Key; - InputStatus status = pair.Value; - - if (status == InputStatus.Pressed) + // get cursor position + ICursorPosition cursor; { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); - - // legacy events - if (button.TryGetKeyboard(out Keys key)) - { - if (key != Keys.None) - this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); - } - else if (button.TryGetController(out Buttons controllerButton)) - { - if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); - else - this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); - } + // cursor position + Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); } - else if (status == InputStatus.Released) + + // raise input events + foreach (var pair in inputState.ActiveButtons) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + SButton button = pair.Key; + InputStatus status = pair.Value; - // legacy events - if (button.TryGetKeyboard(out Keys key)) + if (status == InputStatus.Pressed) { - if (key != Keys.None) - this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + + // legacy events + if (button.TryGetKeyboard(out Keys key)) + { + if (key != Keys.None) + this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + else + this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); + } } - else if (button.TryGetController(out Buttons controllerButton)) + else if (status == InputStatus.Released) { - if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); - else - this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + + // legacy events + if (button.TryGetKeyboard(out Keys key)) + { + if (key != Keys.None) + this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + else + this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + } } } - } - // raise legacy state-changed events - if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); - if (inputState.MouseState != this.PreviousInput.MouseState) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); + // raise legacy state-changed events + if (inputState.KeyboardState != this.PreviousInput.KeyboardState) + this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); + if (inputState.MouseState != this.PreviousInput.MouseState) + this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); + } // track state this.PreviousInput = inputState; -- cgit From 5e7eaf9f75d72d8cbb338c35b43f2974440b3456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 19:59:03 -0400 Subject: rewrite input suppression (#453) This lets SMAPI intercept all input using the new Game1.hooks in SDV 1.3.0.32. However, intercepting mouse clicks needs a few more changes in the game code. --- docs/release-notes.md | 3 +- src/SMAPI/Events/EventArgsInput.cs | 109 ++-------------- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 153 +++++++++++++++++++++++ src/SMAPI/Framework/Input/InputState.cs | 78 +++++++++--- src/SMAPI/Framework/SGame.cs | 78 +++++++++++- src/SMAPI/SModHooks.cs | 48 +++++++ src/SMAPI/StardewModdingAPI.csproj | 2 + 7 files changed, 357 insertions(+), 114 deletions(-) create mode 100644 src/SMAPI/Framework/Input/GamePadStateBuilder.cs create mode 100644 src/SMAPI/SModHooks.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ee2d0aa1..565ed58c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,7 +22,8 @@ * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. - * Overhauled world/player state tracking: + * Rewrote input suppression using new SDV 1.3 APIs. + * Rewrote world/player state tracking: * much more efficient than previous method; * uses net field events where available; * lays groundwork for tracking events for multiple players. diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 0cf0828b..d60f4017 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -1,14 +1,18 @@ using System; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewValley; +using System.Collections.Generic; namespace StardewModdingAPI.Events { /// Event arguments when a button is pressed or released. public class EventArgsInput : EventArgs { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + /********* ** Accessors *********/ @@ -25,7 +29,7 @@ namespace StardewModdingAPI.Events public bool IsUseToolButton { get; } /// Whether a mod has indicated the key was already handled. - public bool IsSuppressed { get; private set; } + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); /********* @@ -36,12 +40,14 @@ namespace StardewModdingAPI.Events /// The cursor position. /// Whether the input should trigger actions on the affected tile. /// Whether the input should use tools on the affected tile. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) + /// The buttons to suppress. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton, HashSet suppressButtons) { this.Button = button; this.Cursor = cursor; this.IsActionButton = isActionButton; this.IsUseToolButton = isUseToolButton; + this.SuppressButtons = suppressButtons; } /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. @@ -54,96 +60,7 @@ namespace StardewModdingAPI.Events /// The button to suppress. public void SuppressButton(SButton button) { - if (button == this.Button) - this.IsSuppressed = true; - - // keyboard - if (button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); - - // controller - else if (button.TryGetController(out Buttons controllerButton)) - { - var newState = GamePad.GetState(PlayerIndex.One); - var thumbsticks = Game1.oldPadState.ThumbSticks; - var triggers = Game1.oldPadState.Triggers; - var buttons = Game1.oldPadState.Buttons; - var dpad = Game1.oldPadState.DPad; - - switch (controllerButton) - { - // d-pad - case Buttons.DPadDown: - dpad = new GamePadDPad(dpad.Up, newState.DPad.Down, dpad.Left, dpad.Right); - break; - case Buttons.DPadLeft: - dpad = new GamePadDPad(dpad.Up, dpad.Down, newState.DPad.Left, dpad.Right); - break; - case Buttons.DPadRight: - dpad = new GamePadDPad(dpad.Up, dpad.Down, dpad.Left, newState.DPad.Right); - break; - case Buttons.DPadUp: - dpad = new GamePadDPad(newState.DPad.Up, dpad.Down, dpad.Left, dpad.Right); - break; - - // trigger - case Buttons.LeftTrigger: - triggers = new GamePadTriggers(newState.Triggers.Left, triggers.Right); - break; - case Buttons.RightTrigger: - triggers = new GamePadTriggers(triggers.Left, newState.Triggers.Right); - break; - - // thumbstick - case Buttons.LeftThumbstickDown: - case Buttons.LeftThumbstickLeft: - case Buttons.LeftThumbstickRight: - case Buttons.LeftThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Left, thumbsticks.Right); - break; - case Buttons.RightThumbstickDown: - case Buttons.RightThumbstickLeft: - case Buttons.RightThumbstickRight: - case Buttons.RightThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Right, thumbsticks.Left); - break; - - // buttons - default: - var mask = - (buttons.A == ButtonState.Pressed ? Buttons.A : 0) - | (buttons.B == ButtonState.Pressed ? Buttons.B : 0) - | (buttons.Back == ButtonState.Pressed ? Buttons.Back : 0) - | (buttons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0) - | (buttons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0) - | (buttons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0) - | (buttons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0) - | (buttons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0) - | (buttons.Start == ButtonState.Pressed ? Buttons.Start : 0) - | (buttons.X == ButtonState.Pressed ? Buttons.X : 0) - | (buttons.Y == ButtonState.Pressed ? Buttons.Y : 0); - mask = mask ^ controllerButton; - buttons = new GamePadButtons(mask); - break; - } - - Game1.oldPadState = new GamePadState(thumbsticks, triggers, buttons, dpad); - } - - // mouse - else if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - { - Game1.oldMouseState = new MouseState( - x: Game1.oldMouseState.X, - y: Game1.oldMouseState.Y, - scrollWheel: Game1.oldMouseState.ScrollWheelValue, - leftButton: button == SButton.MouseLeft ? ButtonState.Pressed : Game1.oldMouseState.LeftButton, - middleButton: button == SButton.MouseMiddle ? ButtonState.Pressed : Game1.oldMouseState.MiddleButton, - rightButton: button == SButton.MouseRight ? ButtonState.Pressed : Game1.oldMouseState.RightButton, - xButton1: button == SButton.MouseX1 ? ButtonState.Pressed : Game1.oldMouseState.XButton1, - xButton2: button == SButton.MouseX2 ? ButtonState.Pressed : Game1.oldMouseState.XButton2 - ); - } + this.SuppressButtons.Add(button); } } } diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs new file mode 100644 index 00000000..5eeb7ef6 --- /dev/null +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Framework.Input +{ + /// An abstraction for manipulating controller state. + internal class GamePadStateBuilder + { + /********* + ** Properties + *********/ + /// The current button states. + private readonly IDictionary ButtonStates; + + /// The left trigger value. + private float LeftTrigger; + + /// The right trigger value. + private float RightTrigger; + + /// The left thumbstick position. + private Vector2 LeftStickPos; + + /// The left thumbstick position. + private Vector2 RightStickPos; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial controller state. + public GamePadStateBuilder(GamePadState state) + { + this.ButtonStates = new Dictionary + { + [SButton.DPadUp] = state.DPad.Up, + [SButton.DPadDown] = state.DPad.Down, + [SButton.DPadLeft] = state.DPad.Left, + [SButton.DPadRight] = state.DPad.Right, + + [SButton.ControllerA] = state.Buttons.A, + [SButton.ControllerB] = state.Buttons.B, + [SButton.ControllerX] = state.Buttons.X, + [SButton.ControllerY] = state.Buttons.Y, + [SButton.LeftStick] = state.Buttons.LeftStick, + [SButton.RightStick] = state.Buttons.RightStick, + [SButton.LeftShoulder] = state.Buttons.LeftShoulder, + [SButton.RightShoulder] = state.Buttons.RightShoulder, + [SButton.ControllerBack] = state.Buttons.Back, + [SButton.ControllerStart] = state.Buttons.Start, + [SButton.BigButton] = state.Buttons.BigButton + }; + this.LeftTrigger = state.Triggers.Left; + this.RightTrigger = state.Triggers.Right; + this.LeftStickPos = state.ThumbSticks.Left; + this.RightStickPos = state.ThumbSticks.Right; + } + + /// Mark all matching buttons unpressed. + /// The buttons. + public void SuppressButtons(IEnumerable buttons) + { + foreach (SButton button in buttons) + this.SuppressButton(button); + } + + /// Mark a button unpressed. + /// The button. + public void SuppressButton(SButton button) + { + switch (button) + { + // left thumbstick + case SButton.LeftThumbstickUp: + if (this.LeftStickPos.Y > 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickDown: + if (this.LeftStickPos.Y < 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickLeft: + if (this.LeftStickPos.X < 0) + this.LeftStickPos.X = 0; + break; + case SButton.LeftThumbstickRight: + if (this.LeftStickPos.X > 0) + this.LeftStickPos.X = 0; + break; + + // right thumbstick + case SButton.RightThumbstickUp: + if (this.RightStickPos.Y > 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickDown: + if (this.RightStickPos.Y < 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickLeft: + if (this.RightStickPos.X < 0) + this.RightStickPos.X = 0; + break; + case SButton.RightThumbstickRight: + if (this.RightStickPos.X > 0) + this.RightStickPos.X = 0; + break; + + // triggers + case SButton.LeftTrigger: + this.LeftTrigger = 0; + break; + case SButton.RightTrigger: + this.RightTrigger = 0; + break; + + // buttons + default: + if (this.ButtonStates.ContainsKey(button)) + this.ButtonStates[button] = ButtonState.Released; + break; + } + } + + /// Construct an equivalent gamepad state. + public GamePadState ToGamePadState() + { + return new GamePadState( + leftThumbStick: this.LeftStickPos, + rightThumbStick: this.RightStickPos, + leftTrigger: this.LeftTrigger, + rightTrigger: this.RightTrigger, + buttons: this.GetPressedButtons().ToArray() + ); + } + + /********* + ** Private methods + *********/ + /// Get all pressed buttons. + private IEnumerable GetPressedButtons() + { + foreach (var pair in this.ButtonStates) + { + if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button)) + yield return button; + } + } + } +} diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs index 7c8676e9..62337a6c 100644 --- a/src/SMAPI/Framework/Input/InputState.cs +++ b/src/SMAPI/Framework/Input/InputState.cs @@ -9,6 +9,16 @@ namespace StardewModdingAPI.Framework.Input /// A summary of input changes during an update frame. internal class InputState { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + /// The maximum amount of direction to ignore for the right thumbstick. + private const float RightThumbstickDeadZone = 0f; + + /********* ** Accessors *********/ @@ -48,7 +58,7 @@ namespace StardewModdingAPI.Framework.Input this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX // get button states - SButton[] down = InputState.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); + SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); foreach (SButton button in down) this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true); foreach (KeyValuePair prev in previousState.ActiveButtons) @@ -109,7 +119,8 @@ namespace StardewModdingAPI.Framework.Input /// The keyboard state. /// The mouse state. /// The controller state. - private static IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) { // keyboard foreach (Keys key in keyboard.GetPressedKeys()) @@ -130,28 +141,23 @@ namespace StardewModdingAPI.Framework.Input // controller if (controller.IsConnected) { + // main buttons if (controller.Buttons.A == ButtonState.Pressed) yield return SButton.ControllerA; if (controller.Buttons.B == ButtonState.Pressed) yield return SButton.ControllerB; - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; if (controller.Buttons.LeftStick == ButtonState.Pressed) yield return SButton.LeftStick; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; if (controller.Buttons.RightStick == ButtonState.Pressed) yield return SButton.RightStick; if (controller.Buttons.Start == ButtonState.Pressed) yield return SButton.ControllerStart; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; + + // directional pad if (controller.DPad.Up == ButtonState.Pressed) yield return SButton.DPadUp; if (controller.DPad.Down == ButtonState.Pressed) @@ -160,11 +166,55 @@ namespace StardewModdingAPI.Framework.Input yield return SButton.DPadLeft; if (controller.DPad.Right == ButtonState.Pressed) yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers if (controller.Triggers.Left > 0.2f) yield return SButton.LeftTrigger; if (controller.Triggers.Right > 0.2f) yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } } } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 35e027d8..3b9a159f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -120,6 +120,9 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + private readonly HashSet SuppressButtons = new HashSet(); + /********* ** Accessors @@ -154,6 +157,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); + Game1.hooks = new SModHooks(this.UpdateControlInput); // init watchers Game1.locations = new ObservableCollection(); @@ -420,7 +424,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -438,7 +442,7 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -539,7 +543,7 @@ namespace StardewModdingAPI.Framework if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) { if (this.VerboseLogging) - this.Monitor.Log($"Context: current location objects changed.", LogLevel.Trace); + this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); } @@ -619,6 +623,17 @@ namespace StardewModdingAPI.Framework } } + /// Read the current input state for handling. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic) + { + this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState); + defaultLogic(); + } + /// The method called to draw everything to the screen. /// A snapshot of the game timing state. protected override void Draw(GameTime gameTime) @@ -1240,6 +1255,63 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ + /// Apply input suppression for the given input states. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + // stop suppressing buttons once released + if (this.SuppressButtons.Count != 0) + { + InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState); + this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p)); + } + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet keyboardButtons = new HashSet(); + HashSet controllerButtons = new HashSet(); + HashSet mouseButtons = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + mouseButtons.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + keyboardButtons.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + controllerButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && controllerButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(controllerButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (mouseButtons.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, + middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, + rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, + xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, + xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + ); + } + } + /// Perform any cleanup needed when the player unloads a save and returns to the title screen. private void CleanupAfterReturnToTitle() { diff --git a/src/SMAPI/SModHooks.cs b/src/SMAPI/SModHooks.cs new file mode 100644 index 00000000..1b88d606 --- /dev/null +++ b/src/SMAPI/SModHooks.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Intercepts predefined Stardew Valley mod hooks. + internal class SModHooks : ModHooks + { + /********* + ** Delegates + *********/ + /// A delegate invoked by the hook. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public delegate void UpdateControlInputDelegate(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action); + + + /********* + ** Properties + *********/ + /// The callback for . + private readonly UpdateControlInputDelegate UpdateControlInputHandler; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The callback for . + public SModHooks(UpdateControlInputDelegate updateControlInputHandler) + { + this.UpdateControlInputHandler = updateControlInputHandler; + } + + /// A hook invoked before the game processes player input. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action) + { + this.UpdateControlInputHandler(ref keyboardState, ref mouseState, ref gamePadState, action); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5fe3e32c..d7719e27 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ + @@ -262,6 +263,7 @@ + -- cgit From b2c4218e0dec8c62906ccf8a762f0bf84e14a74f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 20:20:43 -0400 Subject: update console log suppress patterns for SDV 1.3 (#453) --- src/SMAPI/Program.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 8f91c32b..6133d010 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -90,7 +90,11 @@ namespace StardewModdingAPI private readonly Regex[] SuppressConsolePatterns = { new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant) + new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^Multiplayer auth success$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^DebugOutput: added CLOUD", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; /// Encapsulates SMAPI's JSON file parsing. -- cgit From 2b2ad7a48621e7f70e92cba72b14e56ebe9becb9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 21:15:19 -0400 Subject: fix save/load event precedence (#453) --- src/SMAPI/Framework/SGame.cs | 73 ++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 3b9a159f..4f5bd96b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -236,42 +236,6 @@ namespace StardewModdingAPI.Framework return; } - /********* - ** Update context - *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) - { - if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) - this.AfterLoadTimer--; - Context.IsWorldReady = this.AfterLoadTimer <= 0; - } - - /********* - ** Update watchers - *********/ - // reset player - if (Context.IsWorldReady) - { - if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) - { - this.CurrentPlayerTracker?.Dispose(); - this.CurrentPlayerTracker = new PlayerTracker(Game1.player); - } - } - else - { - if (this.CurrentPlayerTracker != null) - { - this.CurrentPlayerTracker.Dispose(); - this.CurrentPlayerTracker = null; - } - } - - // update values - foreach (IWatcher watcher in this.Watchers) - watcher.Update(); - this.CurrentPlayerTracker?.Update(); - /********* ** Save events + suppress events during save *********/ @@ -325,6 +289,43 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) this.OnGameInitialised(); + + /********* + ** Update context + *********/ + if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) + { + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) + this.AfterLoadTimer--; + Context.IsWorldReady = this.AfterLoadTimer <= 0; + } + + /********* + ** Update watchers + *********/ + // reset player + if (Context.IsWorldReady) + { + if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) + { + this.CurrentPlayerTracker?.Dispose(); + this.CurrentPlayerTracker = new PlayerTracker(Game1.player); + } + } + else + { + if (this.CurrentPlayerTracker != null) + { + this.CurrentPlayerTracker.Dispose(); + this.CurrentPlayerTracker = null; + } + } + + // update values + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + this.CurrentPlayerTracker?.Update(); + /********* ** Locale changed events *********/ -- cgit From fd6c7c73cc4fb192df98cb21fb42f47a5c40a379 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 21:17:16 -0400 Subject: change 'Console.Out' logger to 'game' for clarity Although any mod can write to the console directly, this is rare enough that using 'game' as the logger name is less confusing. --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6133d010..3c6b1cf6 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -362,7 +362,7 @@ namespace StardewModdingAPI // redirect direct console output { - Monitor monitor = this.GetSecondaryMonitor("Console.Out"); + Monitor monitor = this.GetSecondaryMonitor("game"); if (monitor.WriteToConsole) this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); } -- cgit From 86cafc77f55ee2b42f4602911d322760d8683972 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 23 Apr 2018 01:05:02 -0500 Subject: cool pufferchick on hover --- src/SMAPI.Web/Views/Index/Index.cshtml | 7 ++++--- .../wwwroot/Content/images/pufferchick-cool.png | Bin 0 -> 2921 bytes src/SMAPI.Web/wwwroot/Content/js/index.js | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png create mode 100644 src/SMAPI.Web/wwwroot/Content/js/index.js (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 4efb9f8a..c13c94a5 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -4,6 +4,7 @@ @model StardewModdingAPI.Web.ViewModels.IndexModel @section Head { + }

@@ -13,14 +14,14 @@

Get help

diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png new file mode 100644 index 00000000..63eb8970 Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png differ diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js new file mode 100644 index 00000000..54fdf6d9 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -0,0 +1,16 @@ +document.addEventListener('DOMContentLoaded', setupHover, false); + +function setupHover() { + var pufferchick = document.getElementById("pufferchick"); + var downloadLinks = document.getElementsByClassName("download"); + + for (var downloadLink of downloadLinks) { + downloadLink.addEventListener("mouseenter", function () { + pufferchick.src = "Content/images/pufferchick-cool.png"; + }); + + downloadLink.addEventListener("mouseleave", function () { + pufferchick.src = "favicon.ico"; + }); + } +} -- cgit From 2bc9184464261f918abe142de566cd82cf565918 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 23 Apr 2018 01:53:32 -0500 Subject: use jQuery --- src/SMAPI.Web/Views/Index/Index.cshtml | 1 + src/SMAPI.Web/wwwroot/Content/js/index.js | 26 ++++++++++---------------- 2 files changed, 11 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index c13c94a5..8ae23a45 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -4,6 +4,7 @@ @model StardewModdingAPI.Web.ViewModels.IndexModel @section Head { + } diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js index 54fdf6d9..c53592f8 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/index.js +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -1,16 +1,10 @@ -document.addEventListener('DOMContentLoaded', setupHover, false); - -function setupHover() { - var pufferchick = document.getElementById("pufferchick"); - var downloadLinks = document.getElementsByClassName("download"); - - for (var downloadLink of downloadLinks) { - downloadLink.addEventListener("mouseenter", function () { - pufferchick.src = "Content/images/pufferchick-cool.png"; - }); - - downloadLink.addEventListener("mouseleave", function () { - pufferchick.src = "favicon.ico"; - }); - } -} +$(document).ready(function () { + var pufferchick = $("#pufferchick"); + $(".download").each(function (index, element) { + $(element).hover(function () { + pufferchick.attr("src", "Content/images/pufferchick-cool.png"); + }, function () { + pufferchick.attr("src", "favicon.ico"); + }) + }); +}); -- cgit From 82f418a38baeb44f68601708ebbd3c4af03ef6da Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 23 Apr 2018 01:58:18 -0500 Subject: add missing semicolon --- src/SMAPI.Web/wwwroot/Content/js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js index c53592f8..46f78fbe 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/index.js +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -5,6 +5,6 @@ $(document).ready(function () { pufferchick.attr("src", "Content/images/pufferchick-cool.png"); }, function () { pufferchick.attr("src", "favicon.ico"); - }) + }); }); }); -- cgit From 371d7fa05317f107f7b97a7b5db93e176039418d Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Mon, 23 Apr 2018 02:01:14 -0500 Subject: use implicit iteration instead --- src/SMAPI.Web/wwwroot/Content/js/index.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js index 46f78fbe..ac05df05 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/index.js +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -1,10 +1,8 @@ $(document).ready(function () { var pufferchick = $("#pufferchick"); - $(".download").each(function (index, element) { - $(element).hover(function () { - pufferchick.attr("src", "Content/images/pufferchick-cool.png"); - }, function () { - pufferchick.attr("src", "favicon.ico"); - }); + $(".download").hover(function () { + pufferchick.attr("src", "Content/images/pufferchick-cool.png"); + }, function () { + pufferchick.attr("src", "favicon.ico"); }); }); -- cgit From 9cebd83cb968a61e809bd721056fd9fc17f3b5fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 23 Apr 2018 18:15:58 -0400 Subject: show update-key warnings before checking mod status, enable in non-dev mode --- docs/release-notes.md | 2 +- src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 9 ++++++- src/SMAPI/Program.cs | 38 +++++++-------------------- 4 files changed, 22 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 565ed58c..3cb048cd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,13 +3,13 @@ * For players: * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. + * Added warning for mods which don't have update checks configured. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. * Fixed `world_settime` console command sometimes breaking NPC schedules (e.g. so they stay in bed). * For modders: * Added code analysis to mod build config package to flag common issues as warnings. - * Added warning when a mod doesn't have update keys (currently only shown in developer mode). * Fixed assets loaded by temporary content managers not being editable. * Fixed issue where assets didn't reload correctly when the player switches language. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 248809df..b7972fe1 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -65,6 +65,9 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); + /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). + bool HasManifest(); + /// Whether the mod has at least one update key set. bool HasUpdateKeys(); } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index af888b71..d3a33e7a 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -104,11 +104,18 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } + /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). + public bool HasManifest() + { + return this.Manifest != null; + } + /// Whether the mod has at least one update key set. public bool HasUpdateKeys() { return - this.Manifest?.UpdateKeys != null + this.HasManifest() + && this.Manifest.UpdateKeys != null && this.Manifest.UpdateKeys.Any(key => !string.IsNullOrWhiteSpace(key)); } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3c6b1cf6..9789cf85 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -582,25 +582,8 @@ namespace StardewModdingAPI StringComparer.InvariantCultureIgnoreCase ); - // report update keys - { - IModMetadata[] modsWithoutKeys = ( - from mod in mods - where - mod.Manifest != null - && (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) - && (mod.Manifest?.UniqueID != "SMAPI.ConsoleCommands" && mod.Manifest?.UniqueID != "SMAPI.TrainerMod") - orderby mod.DisplayName - select mod - ).ToArray(); - - string message = $"Checking {modsByKey.Count} mod update keys."; - if (modsWithoutKeys.Any()) - message += $" {modsWithoutKeys.Length} mods have no update keys: {string.Join(", ", modsWithoutKeys.Select(p => p.DisplayName))}."; - this.Monitor.Log($" {message}", LogLevel.Trace); - } - // fetch results + this.Monitor.Log($" Checking {modsByKey.Count} mod update keys.", LogLevel.Trace); var results = ( from entry in client.GetModInfo(modsByKey.Keys.ToArray()) @@ -714,10 +697,12 @@ namespace StardewModdingAPI // load content packs foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) { - // get basic info - IManifest manifest = metadata.Manifest; this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)})...", LogLevel.Trace); + // show warning for missing update key + if (metadata.HasManifest() && !metadata.HasUpdateKeys()) + this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + // validate status if (metadata.Status == ModMetadataStatus.Failed) { @@ -726,11 +711,8 @@ namespace StardewModdingAPI continue; } - // show warnings - if (this.Settings.DeveloperMode && !metadata.HasUpdateKeys()) - this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); - // load mod as content pack + IManifest manifest = metadata.Manifest; IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); @@ -766,6 +748,10 @@ namespace StardewModdingAPI ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid : $" {metadata.DisplayName}...", LogLevel.Trace); + // show warnings + if (metadata.HasManifest() && !metadata.HasUpdateKeys() && metadata.Manifest.UniqueID != "SMAPI.ConsoleCommands") + this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + // validate status if (metadata.Status == ModMetadataStatus.Failed) { @@ -774,10 +760,6 @@ namespace StardewModdingAPI continue; } - // show warnings - if (this.Settings.DeveloperMode && !metadata.HasUpdateKeys() && metadata.Manifest.UniqueID != "SMAPI.ConsoleCommands") - this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); - // load mod string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) -- cgit From 3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 02:47:07 -0400 Subject: rewrite input suppression again (#453) This uses the new Game1.input in SDV 1.3.0.37 to override the game's input more consistently, though it still doesn't intercept clicks correctly yet. --- src/SMAPI/Framework/Input/InputState.cs | 220 ------------------- src/SMAPI/Framework/Input/SInputState.cs | 359 +++++++++++++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 113 ++-------- src/SMAPI/SModHooks.cs | 48 ----- src/SMAPI/StardewModdingAPI.csproj | 3 +- 5 files changed, 376 insertions(+), 367 deletions(-) delete mode 100644 src/SMAPI/Framework/Input/InputState.cs create mode 100644 src/SMAPI/Framework/Input/SInputState.cs delete mode 100644 src/SMAPI/SModHooks.cs (limited to 'src') diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs deleted file mode 100644 index 62337a6c..00000000 --- a/src/SMAPI/Framework/Input/InputState.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI.Framework.Input -{ - /// A summary of input changes during an update frame. - internal class InputState - { - /********* - ** Accessors - *********/ - /// The maximum amount of direction to ignore for the left thumbstick. - private const float LeftThumbstickDeadZone = 0.2f; - - /// The maximum amount of direction to ignore for the right thumbstick. - private const float RightThumbstickDeadZone = 0f; - - - /********* - ** Accessors - *********/ - /// The underlying controller state. - public GamePadState ControllerState { get; } - - /// The underlying keyboard state. - public KeyboardState KeyboardState { get; } - - /// The underlying mouse state. - public MouseState MouseState { get; } - - /// The mouse position on the screen adjusted for the zoom level. - public Point MousePosition { get; } - - /// The buttons which were pressed, held, or released. - public IDictionary ActiveButtons { get; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public InputState() { } - - /// Construct an instance. - /// The previous input state. - /// The current controller state. - /// The current keyboard state. - /// The current mouse state. - public InputState(InputState previousState, GamePadState controllerState, KeyboardState keyboardState, MouseState mouseState) - { - // init properties - this.ControllerState = controllerState; - this.KeyboardState = keyboardState; - this.MouseState = mouseState; - this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX - - // get button states - SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); - foreach (SButton button in down) - this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true); - foreach (KeyValuePair prev in previousState.ActiveButtons) - { - if (prev.Value.IsDown() && !this.ActiveButtons.ContainsKey(prev.Key)) - this.ActiveButtons[prev.Key] = InputStatus.Released; - } - } - - /// Get the status of a button. - /// The button to check. - public InputStatus GetStatus(SButton button) - { - return this.ActiveButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; - } - - /// Get whether a given button was pressed or held. - /// The button to check. - public bool IsDown(SButton button) - { - return this.GetStatus(button).IsDown(); - } - - /// Get whether any of the given buttons were pressed or held. - /// The buttons to check. - public bool IsAnyDown(InputButton[] buttons) - { - return buttons.Any(button => this.IsDown(button.ToSButton())); - } - - /// Get the current input state. - /// The previous input state. - public static InputState GetState(InputState previousState) - { - GamePadState controllerState = GamePad.GetState(PlayerIndex.One); - KeyboardState keyboardState = Keyboard.GetState(); - MouseState mouseState = Mouse.GetState(); - - return new InputState(previousState, controllerState, keyboardState, mouseState); - } - - /********* - ** Private methods - *********/ - /// Get the status of a button. - /// The previous button status. - /// Whether the button is currently down. - public InputStatus GetStatus(InputStatus oldStatus, bool isDown) - { - if (isDown && oldStatus.IsDown()) - return InputStatus.Held; - if (isDown) - return InputStatus.Pressed; - return InputStatus.Released; - } - - /// Get the buttons pressed in the given stats. - /// The keyboard state. - /// The mouse state. - /// The controller state. - /// Thumbstick direction logic derived from . - private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) - { - // keyboard - foreach (Keys key in keyboard.GetPressedKeys()) - yield return key.ToSButton(); - - // mouse - if (mouse.LeftButton == ButtonState.Pressed) - yield return SButton.MouseLeft; - if (mouse.RightButton == ButtonState.Pressed) - yield return SButton.MouseRight; - if (mouse.MiddleButton == ButtonState.Pressed) - yield return SButton.MouseMiddle; - if (mouse.XButton1 == ButtonState.Pressed) - yield return SButton.MouseX1; - if (mouse.XButton2 == ButtonState.Pressed) - yield return SButton.MouseX2; - - // controller - if (controller.IsConnected) - { - // main buttons - if (controller.Buttons.A == ButtonState.Pressed) - yield return SButton.ControllerA; - if (controller.Buttons.B == ButtonState.Pressed) - yield return SButton.ControllerB; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; - if (controller.Buttons.LeftStick == ButtonState.Pressed) - yield return SButton.LeftStick; - if (controller.Buttons.RightStick == ButtonState.Pressed) - yield return SButton.RightStick; - if (controller.Buttons.Start == ButtonState.Pressed) - yield return SButton.ControllerStart; - - // directional pad - if (controller.DPad.Up == ButtonState.Pressed) - yield return SButton.DPadUp; - if (controller.DPad.Down == ButtonState.Pressed) - yield return SButton.DPadDown; - if (controller.DPad.Left == ButtonState.Pressed) - yield return SButton.DPadLeft; - if (controller.DPad.Right == ButtonState.Pressed) - yield return SButton.DPadRight; - - // secondary buttons - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - - // shoulders - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; - - // triggers - if (controller.Triggers.Left > 0.2f) - yield return SButton.LeftTrigger; - if (controller.Triggers.Right > 0.2f) - yield return SButton.RightTrigger; - - // left thumbstick direction - if (controller.ThumbSticks.Left.Y > InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickUp; - if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickDown; - if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickRight; - if (controller.ThumbSticks.Left.X < -InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickLeft; - - // right thumbstick direction - if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) - { - if (controller.ThumbSticks.Right.Y > 0) - yield return SButton.RightThumbstickUp; - if (controller.ThumbSticks.Right.Y < 0) - yield return SButton.RightThumbstickDown; - if (controller.ThumbSticks.Right.X > 0) - yield return SButton.RightThumbstickRight; - if (controller.ThumbSticks.Right.X < 0) - yield return SButton.RightThumbstickLeft; - } - } - } - - /// Get whether the right thumbstick should be considered outside the dead zone. - /// The right thumbstick value. - private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) - { - return direction.Length() > 0.9f; - } - } -} diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs new file mode 100644 index 00000000..62defa9f --- /dev/null +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +#pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) +namespace StardewModdingAPI.Framework.Input +{ + /// A summary of input changes during an update frame. + internal sealed class SInputState : InputState + { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + + /********* + ** Accessors + *********/ + /// The controller state as of the last update. + public GamePadState RealController { get; private set; } + + /// The keyboard state as of the last update. + public KeyboardState RealKeyboard { get; private set; } + + /// The mouse state as of the last update. + public MouseState RealMouse { get; private set; } + + /// A derivative of which suppresses the buttons in . + public GamePadState SuppressedController { get; private set; } + + /// A derivative of which suppresses the buttons in . + public KeyboardState SuppressedKeyboard { get; private set; } + + /// A derivative of which suppresses the buttons in . + public MouseState SuppressedMouse { get; private set; } + + /// The mouse position on the screen adjusted for the zoom level. + public Point MousePosition { get; private set; } + + /// The buttons which were pressed, held, or released. + public IDictionary ActiveButtons { get; private set; } = new Dictionary(); + + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + public HashSet SuppressButtons { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Get a copy of the current state. + public SInputState Clone() + { + return new SInputState + { + ActiveButtons = this.ActiveButtons, + RealController = this.RealController, + RealKeyboard = this.RealKeyboard, + RealMouse = this.RealMouse, + MousePosition = this.MousePosition + }; + } + + /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. + [Obsolete("This method should only be called by the game itself.")] + public override void Update() { } + + /// Update the current button statuses for the given tick. + public void TrueUpdate() + { + try + { + // get new states + GamePadState realController = GamePad.GetState(PlayerIndex.One); + KeyboardState realKeyboard = Keyboard.GetState(); + MouseState realMouse = Mouse.GetState(); + Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX + var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); + + // get suppressed states + GamePadState suppressedController = realController; + KeyboardState suppressedKeyboard = realKeyboard; + MouseState suppressedMouse = realMouse; + if (this.SuppressButtons.Count > 0) + this.UpdateSuppression(activeButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + + // update + this.ActiveButtons = activeButtons; + this.RealController = realController; + this.RealKeyboard = realKeyboard; + this.RealMouse = realMouse; + this.SuppressedController = suppressedController; + this.SuppressedKeyboard = suppressedKeyboard; + this.SuppressedMouse = suppressedMouse; + this.MousePosition = mousePosition; + } + catch (InvalidOperationException) + { + // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + } + } + + /// Get the gamepad state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override GamePadState GetGamePadState() + { + return this.ShouldSuppressNow() + ? this.SuppressedController + : this.RealController; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override KeyboardState GetKeyboardState() + { + return this.ShouldSuppressNow() + ? this.SuppressedKeyboard + : this.RealKeyboard; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override MouseState GetMouseState() + { + return this.ShouldSuppressNow() + ? this.SuppressedMouse + : this.RealMouse; + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.GetStatus(this.ActiveButtons, button).IsDown(); + } + + /// Get whether any of the given buttons were pressed or held. + /// The buttons to check. + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + + /// Apply input suppression for the given input states. + /// The current button states to check. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + public void UpdateSuppression(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + // stop suppressing buttons once released + if (this.SuppressButtons.Count != 0) + this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet keyboardButtons = new HashSet(); + HashSet controllerButtons = new HashSet(); + HashSet mouseButtons = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + mouseButtons.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + keyboardButtons.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + controllerButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && controllerButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(controllerButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (mouseButtons.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, + middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, + rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, + xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, + xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + ); + } + } + + + /********* + ** Private methods + *********/ + /// Whether input should be suppressed in the current context. + private bool ShouldSuppressNow() + { + return Game1.chatBox != null && !Game1.chatBox.isActive(); + } + + /// Get the status of all pressed or released buttons relative to their previous status. + /// The previous button statuses. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IDictionary DeriveStatuses(IDictionary previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + IDictionary activeButtons = new Dictionary(); + + // handle pressed keys + SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray(); + foreach (SButton button in down) + activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true); + + // handle released keys + foreach (KeyValuePair prev in activeButtons) + { + if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) + activeButtons[prev.Key] = InputStatus.Released; + } + + return activeButtons; + } + + /// Get the status of a button relative to its previous status. + /// The previous button status. + /// Whether the button is currently down. + private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown) + { + if (isDown && oldStatus.IsDown()) + return InputStatus.Held; + if (isDown) + return InputStatus.Pressed; + return InputStatus.Released; + } + + /// Get the status of a button. + /// The current button states to check. + /// The button to check. + private InputStatus GetStatus(IDictionary activeButtons, SButton button) + { + return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; + } + + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) + { + // main buttons + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + + // directional pad + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } + } + } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4f5bd96b..182b90fc 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -53,6 +53,9 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; + /// Manages input visible to the game. + private SInputState Input => (SInputState)Game1.input; + /// 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 @@ -78,9 +81,6 @@ namespace StardewModdingAPI.Framework /**** ** Game state ****/ - /// The player input as of the previous tick. - private InputState PreviousInput = new InputState(); - /// The underlying watchers for convenience. These are accessible individually as separate properties. private readonly List Watchers = new List(); @@ -120,9 +120,6 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; - /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. - private readonly HashSet SuppressButtons = new HashSet(); - /********* ** Accessors @@ -157,7 +154,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); - Game1.hooks = new SModHooks(this.UpdateControlInput); + Game1.input = new SInputState(); // init watchers Game1.locations = new ObservableCollection(); @@ -289,7 +286,7 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) this.OnGameInitialised(); - + /********* ** Update context *********/ @@ -390,16 +387,9 @@ namespace StardewModdingAPI.Framework *********/ if (Game1.game1.IsActive) { - // get input state - InputState inputState; - try - { - inputState = InputState.GetState(this.PreviousInput); - } - catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true - { - inputState = this.PreviousInput; - } + SInputState previousInputState = this.Input.Clone(); + SInputState inputState = this.Input; + inputState.TrueUpdate(); // raise events bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); @@ -425,7 +415,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -436,14 +426,14 @@ namespace StardewModdingAPI.Framework else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -454,7 +444,7 @@ namespace StardewModdingAPI.Framework else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } @@ -462,14 +452,11 @@ namespace StardewModdingAPI.Framework } // raise legacy state-changed events - if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); - if (inputState.MouseState != this.PreviousInput.MouseState) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); + if (inputState.RealKeyboard != previousInputState.RealKeyboard) + this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + if (inputState.RealMouse != previousInputState.RealMouse) + this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); } - - // track state - this.PreviousInput = inputState; } /********* @@ -624,17 +611,6 @@ namespace StardewModdingAPI.Framework } } - /// Read the current input state for handling. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic) - { - this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState); - defaultLogic(); - } - /// The method called to draw everything to the screen. /// A snapshot of the game timing state. protected override void Draw(GameTime gameTime) @@ -1256,63 +1232,6 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ - /// Apply input suppression for the given input states. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) - { - // stop suppressing buttons once released - if (this.SuppressButtons.Count != 0) - { - InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState); - this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p)); - } - if (this.SuppressButtons.Count == 0) - return; - - // gather info - HashSet keyboardButtons = new HashSet(); - HashSet controllerButtons = new HashSet(); - HashSet mouseButtons = new HashSet(); - foreach (SButton button in this.SuppressButtons) - { - if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - mouseButtons.Add(button); - else if (button.TryGetKeyboard(out Keys key)) - keyboardButtons.Add(key); - else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) - controllerButtons.Add(button); - } - - // suppress keyboard keys - if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) - keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); - - // suppress controller keys - if (gamePadState.IsConnected && controllerButtons.Any()) - { - GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); - builder.SuppressButtons(controllerButtons); - gamePadState = builder.ToGamePadState(); - } - - // suppress mouse buttons - if (mouseButtons.Any()) - { - mouseState = new MouseState( - x: mouseState.X, - y: mouseState.Y, - scrollWheel: mouseState.ScrollWheelValue, - leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, - middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, - rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, - xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, - xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 - ); - } - } - /// Perform any cleanup needed when the player unloads a save and returns to the title screen. private void CleanupAfterReturnToTitle() { diff --git a/src/SMAPI/SModHooks.cs b/src/SMAPI/SModHooks.cs deleted file mode 100644 index 1b88d606..00000000 --- a/src/SMAPI/SModHooks.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI -{ - /// Intercepts predefined Stardew Valley mod hooks. - internal class SModHooks : ModHooks - { - /********* - ** Delegates - *********/ - /// A delegate invoked by the hook. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public delegate void UpdateControlInputDelegate(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action); - - - /********* - ** Properties - *********/ - /// The callback for . - private readonly UpdateControlInputDelegate UpdateControlInputHandler; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The callback for . - public SModHooks(UpdateControlInputDelegate updateControlInputHandler) - { - this.UpdateControlInputHandler = updateControlInputHandler; - } - - /// A hook invoked before the game processes player input. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action) - { - this.UpdateControlInputHandler(ref keyboardState, ref mouseState, ref gamePadState, action); - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index d7719e27..560d7bf4 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -92,7 +92,7 @@ - + @@ -263,7 +263,6 @@ - -- cgit From 8ce0862c123f85c22d969187cd7b5c560d9f41e7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 02:47:42 -0400 Subject: update nuget package for upcoming release --- src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs | 4 ++-- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs index 96bd29f4..d6f8dd7f 100644 --- a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs +++ b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Reflection; [assembly: AssemblyTitle("SMAPI.ModBuildConfig")] [assembly: AssemblyDescription("")] -[assembly: AssemblyVersion("2.0.2.0")] -[assembly: AssemblyFileVersion("2.0.2.0")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 2512c4d6..66d02537 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1-alpha20180414 + 2.1 Build package for SMAPI mods Pathoschild Pathoschild -- cgit From f95c7f25f4447fd1a34400a273256b1e5b4df7b1 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Wed, 25 Apr 2018 02:06:05 -0500 Subject: fix not adding last arg --- src/SMAPI/Framework/CommandManager.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 78e03827..0c48d8ec 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -126,6 +126,9 @@ namespace StardewModdingAPI.Framework else currentArg.Add(c); } + + args.Add(string.Concat(currentArg)); + return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); } -- cgit From 151789caa98025632048a1a2d7feb3b64c70829e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 11:39:35 -0400 Subject: add default GOG install path --- build/common.targets | 1 + docs/release-notes.md | 1 + src/SMAPI.ModBuildConfig/build/smapi.targets | 1 + 3 files changed, 3 insertions(+) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 95885299..588eea1b 100644 --- a/build/common.targets +++ b/build/common.targets @@ -14,6 +14,7 @@ $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4fec0c3d..624891eb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * For players: * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added warning for mods which don't have update checks configured. + * Fixed detection of GOG install path in rare cases. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. * Fixed `world_settime` console command sometimes breaking NPC schedules (e.g. so they stay in bed). diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 78afa7da..d33a9637 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -45,6 +45,7 @@ C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) -- cgit From bd146e74e8970156da82684a821e5d29ad054b97 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 12:04:14 -0400 Subject: update release notes, minor tweaks (#475) --- docs/release-notes.md | 1 + src/SMAPI/Framework/Monitor.cs | 9 +++++---- src/SMAPI/Program.cs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 624891eb..38a8b00d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Fixed assets loaded by temporary content managers not being editable. * Fixed issue where assets didn't reload correctly when the player switches language. + * Fixed user command input not saved to log file. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * **Breaking change**: dropped some deprecated APIs. * **Breaking change**: mods can't intercept chatbox input, including the game's hotkey to toggle the chatbox (default `T`). diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index b65932b2..210a059d 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -100,10 +100,11 @@ namespace StardewModdingAPI.Framework this.LogFile.WriteLine(""); } - /// Writes user input to the log file. - /// The input to write. - internal void LogUserInputToFile(string input) + /// Log console input from the user. + /// The user input to log. + internal void LogUserInput(string input) { + // user input already appears in the console, so just need to write to file if (this.WriteToFile) { string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); @@ -158,7 +159,7 @@ namespace StardewModdingAPI.Framework this.LogFile.WriteLine(fullMessage); } - /// Generates a message prefix for the current time. + /// Generate a message prefix for the current time. /// The name of the mod logging the message. /// The log level. private string GenerateMessagePrefix(string source, LogLevel level) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 73e8bb07..f1152d82 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -449,7 +449,7 @@ namespace StardewModdingAPI continue; // write input to log file - this.Monitor.LogUserInputToFile(input); + this.Monitor.LogUserInput(input); // parse input try -- cgit From f8ffdef321f6818d9ff6e2a763501f92e4a05c8b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 12:06:02 -0400 Subject: remove unused monitor setting --- src/SMAPI/Framework/Monitor.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 210a059d..cc511ed4 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -48,9 +48,6 @@ namespace StardewModdingAPI.Framework /// Whether to write anything to the console. This should be disabled if no console is available. internal bool WriteToConsole { get; set; } = true; - /// Whether to write anything to the log file. This should almost always be enabled. - internal bool WriteToFile { get; set; } = true; - /********* ** Public methods @@ -96,8 +93,7 @@ namespace StardewModdingAPI.Framework { if (this.WriteToConsole) this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); - if (this.WriteToFile) - this.LogFile.WriteLine(""); + this.LogFile.WriteLine(""); } /// Log console input from the user. @@ -105,11 +101,8 @@ namespace StardewModdingAPI.Framework internal void LogUserInput(string input) { // user input already appears in the console, so just need to write to file - if (this.WriteToFile) - { - string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); - this.LogFile.WriteLine($"{prefix} $>{input}"); - } + string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); + this.LogFile.WriteLine($"{prefix} $>{input}"); } @@ -155,8 +148,7 @@ namespace StardewModdingAPI.Framework } // write to log file - if (this.WriteToFile) - this.LogFile.WriteLine(fullMessage); + this.LogFile.WriteLine(fullMessage); } /// Generate a message prefix for the current time. -- cgit From cd3dbc47aa6e112d8695bcb9ed81f0b422f21e5c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 12:11:28 -0400 Subject: update release notes, tweak formatting (#477) --- docs/release-notes.md | 3 +++ src/SMAPI.Web/wwwroot/Content/js/index.js | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 38a8b00d..308007bb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,6 +18,9 @@ * **Breaking change**: dropped some deprecated APIs. * **Breaking change**: mods can't intercept chatbox input, including the game's hotkey to toggle the chatbox (default `T`). +* For the log parser: + * The pufferchick is now more stylish. + * For SMAPI developers: * Added more consistent crossplatform handling using a new `EnvironmentUtility`. * Added MacOS detection. diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js index ac05df05..016d5fa4 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/index.js +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -1,8 +1,11 @@ $(document).ready(function () { var pufferchick = $("#pufferchick"); - $(".download").hover(function () { - pufferchick.attr("src", "Content/images/pufferchick-cool.png"); - }, function () { - pufferchick.attr("src", "favicon.ico"); - }); + $(".download").hover( + function () { + pufferchick.attr("src", "Content/images/pufferchick-cool.png"); + }, + function () { + pufferchick.attr("src", "favicon.ico"); + } + ); }); -- cgit From 0134f0b28d355766a17b1c9da89b8173f978d195 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 13:16:25 -0400 Subject: update release notes, refactor a bit (#474) --- docs/release-notes.md | 1 + .../Framework/Commands/Player/AddCommand.cs | 123 +++++++++------------ .../Framework/ItemData/SearchableItem.cs | 21 +++- src/SMAPI/Framework/CommandManager.cs | 21 ++-- 4 files changed, 85 insertions(+), 81 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 308007bb..6842c941 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. + * Added `player_add name` console command, which lets you add items to your inventory by name instead of ID. * Fixed assets loaded by temporary content managers not being editable. * Fixed issue where assets didn't reload correctly when the player switches language. * Fixed user command input not saved to log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 803ae7f6..71c3ff98 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; @@ -16,16 +15,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Provides methods for searching and constructing items. private readonly ItemRepository Items = new ItemRepository(); - /// All possible item types along with Name. - private readonly string[] ItemTypeAndName = Enum.GetNames(typeof(ItemType)).Union(new string[] { "Name" }).ToArray(); + /// The type names recognised by this command. + private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray(); + /********* ** Public methods *********/ /// Construct an instance. public AddCommand() - : base("player_add", AddCommand.GetDescription()) - { } + : base("player_add", AddCommand.GetDescription()) { } /// Handle the command. /// Writes messages to the console and log file. @@ -40,24 +39,21 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player return; } - SearchableItem match; - - //read arguments - if (!args.TryGet(0, "item type", out string typeOrName, oneOf: this.ItemTypeAndName)) - return; - if (Enum.GetNames(typeof(ItemType)).Contains(typeOrName, StringComparer.InvariantCultureIgnoreCase)) - this.FindItemByTypeAndId(monitor, args, typeOrName, out match); - else - this.FindItemByName(monitor, args, out match); - - if (match == null) + // read arguments + if (!args.TryGet(0, "item type", out string type, oneOf: this.ValidTypes)) return; - if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) count = 1; if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) quality = Object.lowQuality; + // find matching item + SearchableItem match = Enum.TryParse(type, true, out ItemType itemType) + ? this.FindItemByID(monitor, args, itemType) + : this.FindItemByName(monitor, args); + if (match == null) + return; + // apply count match.Item.Stack = count; @@ -72,90 +68,81 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); } + /********* ** Private methods *********/ - - /// Finds a matching item by item type and id. + /// Get a matching item by its ID. /// Writes messages to the console and log file. /// The command arguments. - /// The raw item type. - /// The matching item. - private void FindItemByTypeAndId(IMonitor monitor, ArgumentParser args, string rawType, out SearchableItem match) + /// The item type. + private SearchableItem FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type) { - match = null; - // read arguments if (!args.TryGetInt(1, "item ID", out int id, min: 0)) - return; - - ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true); + return null; // find matching item - match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); - - if (match == null) - { + SearchableItem item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); + if (item == null) monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); - } + return item; } - /// Finds a matching item by name. + /// Get a matching item by its name. /// Writes messages to the console and log file. /// The command arguments. - /// The item name. - /// The matching item. - private void FindItemByName(IMonitor monitor, ArgumentParser args, out SearchableItem match) + private SearchableItem FindItemByName(IMonitor monitor, ArgumentParser args) { - match = null; - // read arguments if (!args.TryGet(1, "item name", out string name)) - return; + return null; // find matching items - IEnumerable matching = this.Items.GetAll().Where(p => p.DisplayName.IndexOf(name, StringComparison.InvariantCultureIgnoreCase) != -1); - match = matching.FirstOrDefault(item => item.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase)); - - // handle unique requirement - if (match != null) - { - return; - } - - int numberOfMatches = matching.Count(); - - if (numberOfMatches == 0) - { - monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error); - } - else if (numberOfMatches == 1) + SearchableItem[] matches = this.Items.GetAll().Where(p => p.NameContains(name)).ToArray(); + switch (matches.Length) { - monitor.Log($"There's no item with name '{name}'. Did you mean '{matching.ElementAt(0).DisplayName}'? If so, type 'player_add name {matching.ElementAt(0).DisplayName}'.", LogLevel.Error); - } - else - { - string options = this.GetTableString(matching, new string[] { "type", "name", "command" }, item => new string[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" }); - - monitor.Log($"Found multiple item names containing '{name}'. Type one of these commands for the one you want:", LogLevel.Error); - monitor.Log($"\n{options}", LogLevel.Info); + // none found + case 0: + monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error); + return null; + + // exact match + case 1 when matches[0].NameEquivalentTo(name): + return matches[0]; + + // list matches + default: + string options = this.GetTableString( + data: matches, + header: new[] { "type", "name", "command" }, + getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" } + ); + monitor.Log($"There's no item with name '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info); + return null; } } + /// Get the command description. private static string GetDescription() { string[] typeValues = Enum.GetNames(typeof(ItemType)); return "Gives the player an item.\n" + "\n" - + "Usage: player_add (|) [count] [quality]\n" - + $"- type: the item type (either Name or one of {string.Join(", ", typeValues)}).\n" + + "Usage: player_add [count] [quality]\n" + + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" + "- item: the item ID (use the 'list_items' command to see a list).\n" - + "- name: the display name of the item (when using type Name).\n" + "- count (optional): how many of the item to give.\n" + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + "\n" - + "This example adds the galaxy sword to your inventory:\n" - + " player_add weapon 4"; + + "Usage: player_add name \"\" [count] [quality]\n" + + "- name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items.\n" + + "- count (optional): how many of the item to give.\n" + + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + + "\n" + + "These examples both add the galaxy sword to your inventory:\n" + + " player_add weapon 4\n" + + " player_add name \"Galaxy Sword\""; } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs index 3eede413..b618a308 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs @@ -1,4 +1,5 @@ -using StardewValley; +using System; +using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData { @@ -37,5 +38,23 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData this.ID = id; this.Item = item; } + + /// Get whether the item name contains a case-insensitive substring. + /// The substring to find. + public bool NameContains(string substring) + { + return + this.Name.IndexOf(substring, StringComparison.InvariantCultureIgnoreCase) != -1 + || this.DisplayName.IndexOf(substring, StringComparison.InvariantCultureIgnoreCase) != -1; + } + + /// Get whether the item name is exactly equal to a case-insensitive string. + /// The substring to find. + public bool NameEquivalentTo(string name) + { + return + this.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase) + || this.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase); + } } } diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 0c48d8ec..f9651ed9 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; namespace StardewModdingAPI.Framework { @@ -103,31 +104,27 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ - /// - /// Parses a string into an array of arguments. - /// + /// Parse a string into command arguments. /// The string to parse. private string[] ParseArgs(string input) { bool inQuotes = false; IList args = new List(); - IList currentArg = new List(); - foreach (char c in input) + StringBuilder currentArg = new StringBuilder(); + foreach (char ch in input) { - if (c == '"') - { + if (ch == '"') inQuotes = !inQuotes; - } - else if (!inQuotes && char.IsWhiteSpace(c)) + else if (!inQuotes && char.IsWhiteSpace(ch)) { - args.Add(string.Concat(currentArg)); + args.Add(currentArg.ToString()); currentArg.Clear(); } else - currentArg.Add(c); + currentArg.Append(ch); } - args.Add(string.Concat(currentArg)); + args.Add(currentArg.ToString()); return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); } -- cgit From 05369f2b317c8e32ff4dec3ffe7aaf06d38d1c32 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 13:29:08 -0400 Subject: add friendly error when game can't load audio (#472) --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6842c941..86475d6e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * For players: * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added warning for mods which don't have update checks configured. + * Added friendly error when game can't start audio. * Fixed detection of GOG install path in rare cases. * Fixed SMAPI update checks not showing newer beta versions when using a beta version. * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f1152d82..eda85866 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -259,6 +259,12 @@ namespace StardewModdingAPI StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window this.GameInstance.Run(); } + catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) + { + this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); + this.PressAnyKeyToExit(); + } catch (Exception ex) { this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); -- cgit From ff571701b21f1f1a0f5c914bdb756312f07fb134 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 19:24:55 -0400 Subject: fix a few implicit net field conversions (#453) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 37623f32..f057eebe 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -351,7 +351,7 @@ namespace StardewModdingAPI.Metadata // update sprites Texture2D texture = content.Load(key); foreach (TAnimal animal in animals) - this.SetSpriteTexture(animal.sprite, texture); + this.SetSpriteTexture(animal.Sprite, texture); return true; } @@ -374,8 +374,8 @@ namespace StardewModdingAPI.Metadata // get expected key string expectedKey = animal.age.Value < animal.ageWhenMature.Value ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}" - : animal.type; - if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce.Value <= 0) + : animal.type.Value; + if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value <= 0) expectedKey = $"Sheared{expectedKey}"; expectedKey = $"Animals\\{expectedKey}"; @@ -426,7 +426,7 @@ namespace StardewModdingAPI.Metadata from location in this.GetLocations() from fence in location.Objects.Values.OfType() where fenceType == 1 - ? fence.isGate + ? fence.isGate.Value : fence.whichType.Value == fenceType select fence ) @@ -562,7 +562,7 @@ namespace StardewModdingAPI.Metadata { foreach (Building building in buildableLocation.buildings) { - GameLocation indoors = building.indoors; + GameLocation indoors = building.indoors.Value; if (indoors != null) yield return indoors; } -- cgit From ae956d0ad8847a8b61d964bd40238de75768260a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 19:25:34 -0400 Subject: update for SDV 1.3.0.38 (#453) --- src/SMAPI/Framework/SGame.cs | 2 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 182b90fc..7ea83957 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -533,7 +533,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().netObjects.FieldDict)); } // raise time changed diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 81e074ec..f6bfc98e 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { @@ -100,7 +100,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.Remove(this.LocationObjectsWatcher); this.LocationObjectsWatcher.Dispose(); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.Watchers.Add(this.LocationObjectsWatcher); } -- cgit From 469e0b8972bf79e5f22e5be4c1dcb8501cc37de4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 19:31:05 -0400 Subject: update mod build package version (#453) --- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 66d02537..5e0b479e 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1 + 2.1.0-beta Build package for SMAPI mods Pathoschild Pathoschild -- cgit From 230099692635a7ca53edfebf4ed9225b5b1d5779 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 20:41:52 -0400 Subject: simplify beta channel logic (#457) --- docs/release-notes.md | 2 +- src/SMAPI/Framework/Models/SConfig.cs | 3 +++ src/SMAPI/Program.cs | 29 +++++++++++------------------ src/SMAPI/StardewModdingAPI.config.json | 6 ++++++ 4 files changed, 21 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 86475d6e..5e14f34d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,8 +4,8 @@ * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. * Added warning for mods which don't have update checks configured. * Added friendly error when game can't start audio. + * Added beta update channel. * Fixed detection of GOG install path in rare cases. - * Fixed SMAPI update checks not showing newer beta versions when using a beta version. * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. * Fixed `world_settime` console command sometimes breaking NPC schedules (e.g. so they stay in bed). diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b504f38b..be84a6b9 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } + /// Whether to show beta versions as valid updates. + public bool UseBetaChannel { get; set; } = Constants.ApiVersion.Build != null; + /// SMAPI's GitHub project name, used to perform update checks. public string GitHubProjectName { get; set; } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index eda85866..4075dae4 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -550,15 +550,18 @@ namespace StardewModdingAPI try { ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; + ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; + if (response.Error != null) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log($"Error: {response.Error}"); } - else if (this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.Version))) - this.Monitor.Log($"You can update SMAPI to {response.Version}: {Constants.HomePageUrl}", LogLevel.Alert); - else if (response.PreviewVersion != null && this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.PreviewVersion))) - this.Monitor.Log($"You can update SMAPI to {response.PreviewVersion}: {Constants.HomePageUrl}", LogLevel.Alert); + else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) + this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); + else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) + this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); else this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } @@ -658,22 +661,12 @@ namespace StardewModdingAPI /// Get whether a given version should be offered to the user as an update. /// The current semantic version. /// The target semantic version. - private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion) + /// Whether the user enabled the beta channel and should be offered pre-release updates. + private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) { - // basic eligibility - bool isNewer = newVersion.IsNewerThan(currentVersion); - bool isPrerelease = newVersion.Build != null; - bool isEquallyStable = !isPrerelease || currentVersion.Build != null; // don't update stable => prerelease - if (!isNewer || !isEquallyStable) - return false; - if (!isPrerelease) - return true; - - // prerelease eligible if same version (excluding prerelease tag) return - newVersion.MajorVersion == currentVersion.MajorVersion - && newVersion.MinorVersion == currentVersion.MinorVersion - && newVersion.PatchVersion == currentVersion.PatchVersion; + newVersion.IsNewerThan(currentVersion) + && (useBetaChannel || newVersion.Build == null); } /// Create a directory path if it doesn't exist. diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 06c63e8b..f37c6fc1 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -21,6 +21,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "CheckForUpdates": true, + /** + * Whether SMAPI should show newer beta versions as an available update. If not specified, SMAPI + * will only show beta updates if the current version is beta. + */ + //"UseBetaChannel": true, + /** * SMAPI's GitHub project name, used to perform update checks. */ -- cgit From efff9723609d66134ca7bc91ecf04436be3c4e05 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 20:49:37 -0400 Subject: update for upcoming 2.6 beta release --- build/GlobalAssemblyInfo.cs | 4 ++-- docs/release-notes.md | 36 +++++++++++++--------------- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 4 files changed, 22 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs index f2477486..bc0ddf69 100644 --- a/build/GlobalAssemblyInfo.cs +++ b/build/GlobalAssemblyInfo.cs @@ -1,5 +1,5 @@ using System.Reflection; [assembly: AssemblyProduct("SMAPI")] -[assembly: AssemblyVersion("2.5.5")] -[assembly: AssemblyFileVersion("2.5.5")] +[assembly: AssemblyVersion("2.6.0")] +[assembly: AssemblyFileVersion("2.6.0")] diff --git a/docs/release-notes.md b/docs/release-notes.md index 5e14f34d..93855ba9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,34 +1,32 @@ # Release notes -## 2.6 alpha +## 2.6 beta * For players: - * Added support for Stardew Valley 1.3+; no longer compatible with earlier versions. - * Added warning for mods which don't have update checks configured. - * Added friendly error when game can't start audio. + * Updated for Stardew Valley 1.3 (no longer compatible with earlier versions). * Added beta update channel. - * Fixed detection of GOG install path in rare cases. - * Fixed console color scheme on Mac or PowerShell, and added override option to `StardewModdingAPI.config.json`. - * Fixed `world_settime` console command sometimes breaking NPC schedules (e.g. so they stay in bed). + * Added friendly error when game can't start audio. + * Added console warning for mods which don't have update checks configured. + * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. + * Fixed detection of GOG Galaxy install path in rare cases. * For modders: * Added code analysis to mod build config package to flag common issues as warnings. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. - * Added `player_add name` console command, which lets you add items to your inventory by name instead of ID. - * Fixed assets loaded by temporary content managers not being editable. - * Fixed issue where assets didn't reload correctly when the player switches language. - * Fixed user command input not saved to log file. + * Fixed assets loaded by temporary content managers not being editable by mods. + * Fixed assets not reloaded consistently when the player switches language. + * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * **Breaking change**: dropped some deprecated APIs. - * **Breaking change**: mods can't intercept chatbox input, including the game's hotkey to toggle the chatbox (default `T`). + * **Breaking change**: mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). -* For the log parser: - * The pufferchick is now more stylish. +* In console commands: + * Added `player_add name`, which lets you add items to your inventory by name instead of ID. + * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * For SMAPI developers: - * Added more consistent crossplatform handling using a new `EnvironmentUtility`. - * Added MacOS detection. - * Added prerelease versions to the mod update-check API response where available (GitHub only). - * Added support for beta releases on the home page. - * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. + * Added more consistent crossplatform handling using a new `EnvironmentUtility`, including MacOS detection. + * Added beta update channel to SMAPI, the web API, and home page. + * Added more stylish pufferchick on the home page. + * Split mod DB out of `StardewModdingAPI.config.json` into its own file. * Rewrote input suppression using new SDV 1.3 APIs. * Rewrote world/player state tracking: * much more efficient than previous method; diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index b6f9b81c..f89049c6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.5.5", + "Version": "2.6.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 1faaf656..2bb06df9 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion($"2.6-alpha.{DateTime.UtcNow:yyyyMMddHHmm}"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.0.27"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.0.38"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From c1f848f09dcc033d05d64eb69cce87c2f89df6a3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 Apr 2018 00:13:07 -0400 Subject: add Summit Rain Fix to compatibility list (#453) --- src/SMAPI/StardewModdingAPI.metadata.json | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index f7a30537..1be5d3b1 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -1561,6 +1561,12 @@ "Default | UpdateKey": "Nexus:691" }, + "Summit Rain Fix": { + "ID": "KoihimeNakamura.summitreborn", + "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) + }, + "Super Greenhouse Warp Modifier": { "ID": "SuperGreenhouse", "Default | UpdateKey": "Chucklefish:4334", -- cgit From 5fc706c18267f5ff4cf31fd0e1ab76f1a57e1588 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 Apr 2018 18:19:36 -0400 Subject: fix error in new update-check logic (#457) --- src/SMAPI/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 4075dae4..dc9e5308 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -665,7 +665,8 @@ namespace StardewModdingAPI private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) { return - newVersion.IsNewerThan(currentVersion) + newVersion != null + && newVersion.IsNewerThan(currentVersion) && (useBetaChannel || newVersion.Build == null); } -- cgit From 83f89c6ef31b783bd6afa4782df14cbbace0f022 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 Apr 2018 23:18:53 -0400 Subject: don't warn when converting net fields to an interface they implement --- .../NetFieldAnalyzer.cs | 25 +++------------------- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 2 files changed, 4 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index e3c92617..0e9154a1 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -19,12 +18,6 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// The namespace for Stardew Valley's Netcode types. private const string NetcodeNamespace = "Netcode"; - /// The full name for Stardew Valley's Netcode.NetList type. - private readonly string NetListTypeFullName = "Netcode.NetList"; - - /// The full name for Stardew Valley's Netcode.NetCollection type. - private readonly string NetCollectionTypeFullName = "Netcode.NetCollection"; - /// Maps net fields to their equivalent non-net properties where available. private readonly IDictionary NetFieldWrapperProperties = new Dictionary { @@ -226,21 +219,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer if (!this.IsNetType(typeInfo.Type) || this.IsNetType(typeInfo.ConvertedType)) return false; - // list conversion to an implemented interface is OK - if (AnalyzerUtilities.GetConcreteTypes(typeInfo.Type).Any(p => p.ToString().StartsWith(this.NetListTypeFullName))) // StartsWith to ignore generics - { - string toType = typeInfo.ConvertedType.ToString(); - if (toType.StartsWith(typeof(IEnumerable<>).Namespace) || toType == typeof(IEnumerable).FullName) - return false; - } - - // collection conversion to an implemented interface is OK - if (AnalyzerUtilities.GetConcreteTypes(typeInfo.Type).Any(p => p.ToString().StartsWith(this.NetCollectionTypeFullName))) // StartsWith to ignore generics - { - string toType = typeInfo.ConvertedType.ToString(); - if (toType.StartsWith(typeof(IEnumerable<>).Namespace) || toType == typeof(IEnumerable).FullName) - return false; - } + // conversion to implemented interface is OK + if (typeInfo.Type.AllInterfaces.Contains(typeInfo.ConvertedType)) + return false; // avoid any other conversions return true; diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 5e0b479e..6bb7736c 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0-beta + 2.1.0-beta-20180426 Build package for SMAPI mods Pathoschild Pathoschild -- cgit From b7edf31c257aad563d12988cf3e99b00a1434ef4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Apr 2018 17:08:24 -0400 Subject: update game version parsing for upcoming SMAPI 2.6 beta (#453) --- src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/GameVersion.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 2bb06df9..8641502f 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -41,7 +41,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.0.38"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.1"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 85b2eef6..261de374 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -49,9 +49,6 @@ namespace StardewModdingAPI.Framework /// The game version string. private static string GetSemanticVersionString(string gameVersion) { - if (gameVersion.StartsWith("1.3.0.")) - return new SemanticVersion(1, 3, 0, "alpha." + gameVersion.Substring("1.3.0.".Length)).ToString(); - return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) ? semanticVersion : gameVersion; @@ -61,9 +58,6 @@ namespace StardewModdingAPI.Framework /// The semantic version string. private static string GetGameVersionString(string semanticVersion) { - if (semanticVersion.StartsWith("1.3-alpha.")) - return "1.3.0." + semanticVersion.Substring("1.3-alpha.".Length); - foreach (var mapping in GameVersion.VersionMap) { if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase)) -- cgit From adda9611c73163270cbfcd34d6617560f81d54b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Apr 2018 18:49:13 -0400 Subject: add multiplayer sync events (#479) --- src/SMAPI/Events/MultiplayerEvents.cs | 58 ++++++++++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 20 +++++++++++ src/SMAPI/Framework/SGame.cs | 1 + src/SMAPI/Framework/SMultiplayer.cs | 47 ++++++++++++++++++++++++ src/SMAPI/Program.cs | 1 + src/SMAPI/StardewModdingAPI.csproj | 2 ++ 6 files changed, 129 insertions(+) create mode 100644 src/SMAPI/Events/MultiplayerEvents.cs create mode 100644 src/SMAPI/Framework/SMultiplayer.cs (limited to 'src') diff --git a/src/SMAPI/Events/MultiplayerEvents.cs b/src/SMAPI/Events/MultiplayerEvents.cs new file mode 100644 index 00000000..f96ecba5 --- /dev/null +++ b/src/SMAPI/Events/MultiplayerEvents.cs @@ -0,0 +1,58 @@ +using System; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised during the multiplayer sync process. + public static class MultiplayerEvents + { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised before the game syncs changes from other players. + public static event EventHandler BeforeMainSync + { + add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainSync.Remove(value); + } + + /// Raised after the game syncs changes from other players. + public static event EventHandler AfterMainSync + { + add => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainSync.Remove(value); + } + + /// Raised before the game broadcasts changes to other players. + public static event EventHandler BeforeMainBroadcast + { + add => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_BeforeMainBroadcast.Remove(value); + } + + /// Raised after the game broadcasts changes to other players. + public static event EventHandler AfterMainBroadcast + { + add => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Add(value); + remove => MultiplayerEvents.EventManager.Multiplayer_AfterMainBroadcast.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + MultiplayerEvents.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index d7c89a76..87ff760f 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -125,6 +125,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised after a game menu is closed. public readonly ManagedEvent Menu_Closed; + /**** + ** MultiplayerEvents + ****/ + /// Raised before the game syncs changes from other players. + public readonly ManagedEvent Multiplayer_BeforeMainSync; + + /// Raised after the game syncs changes from other players. + public readonly ManagedEvent Multiplayer_AfterMainSync; + + /// Raised before the game broadcasts changes to other players. + public readonly ManagedEvent Multiplayer_BeforeMainBroadcast; + + /// Raised after the game broadcasts changes to other players. + public readonly ManagedEvent Multiplayer_AfterMainBroadcast; + /**** ** MineEvents ****/ @@ -228,6 +243,11 @@ namespace StardewModdingAPI.Framework.Events this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + this.Multiplayer_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); + this.Multiplayer_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); + this.Multiplayer_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); + this.Multiplayer_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); + this.Mine_LevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 7ea83957..89bf18ef 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -155,6 +155,7 @@ namespace StardewModdingAPI.Framework if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); Game1.input = new SInputState(); + Game1.multiplayer = new SMultiplayer(monitor, eventManager); // init watchers Game1.locations = new ObservableCollection(); diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs new file mode 100644 index 00000000..687b1922 --- /dev/null +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -0,0 +1,47 @@ +using StardewModdingAPI.Framework.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's implementation of the game's core multiplayer logic. + internal class SMultiplayer : Multiplayer + { + /********* + ** Properties + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Manages SMAPI events. + private readonly EventManager EventManager; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + /// Manages SMAPI events. + public SMultiplayer(IMonitor monitor, EventManager eventManager) + { + this.Monitor = monitor; + this.EventManager = eventManager; + } + + /// Handle sync messages from other players and perform other initial sync logic. + public override void UpdateEarly() + { + this.EventManager.Multiplayer_BeforeMainSync.Raise(); + base.UpdateEarly(); + this.EventManager.Multiplayer_AfterMainSync.Raise(); + } + + /// Broadcast sync messages to other players and perform other final sync logic. + public override void UpdateLate(bool forceSync = false) + { + this.EventManager.Multiplayer_BeforeMainBroadcast.Raise(); + base.UpdateLate(forceSync); + this.EventManager.Multiplayer_AfterMainBroadcast.Raise(); + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index dc9e5308..2325e79a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -157,6 +157,7 @@ namespace StardewModdingAPI LocationEvents.Init(this.EventManager); MenuEvents.Init(this.EventManager); MineEvents.Init(this.EventManager); + MultiplayerEvents.Init(this.EventManager); PlayerEvents.Init(this.EventManager); SaveEvents.Init(this.EventManager); SpecialisedEvents.Init(this.EventManager); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 560d7bf4..f6a16e5f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -132,6 +133,7 @@ + -- cgit From beb2f9c1484626e03acfecffd0f90cd2cddb8cc6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Apr 2018 19:59:41 -0400 Subject: remove alias no longer needed in SDV 1.3 (#453) --- .../NetFieldAnalyzerTests.cs | 3 +-- .../Framework/Commands/Player/SetLevelCommand.cs | 15 +++++++-------- src/SMAPI/Framework/SGame.cs | 11 +++++------ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 5 ++--- 4 files changed, 15 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 7b410085..ab3f2c5e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -19,7 +19,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests using StardewValley; using Netcode; using SObject = StardewValley.Object; - using SFarmer = StardewValley.Farmer; namespace SampleMod { @@ -34,7 +33,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests "; /// The line number where the unit tested code is injected into . - private const int SampleCodeLine = 14; + private const int SampleCodeLine = 13; /// The column number where the unit tested code is injected into . private const int SampleCodeColumn = 25; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs index 68891267..97a36066 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using StardewValley; -using SFarmer = StardewValley.Farmer; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { @@ -11,7 +10,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ** Properties *********/ /// The experience points needed to reach each level. - /// Derived from . + /// Derived from . private readonly IDictionary LevelExp = new Dictionary { [0] = 0, @@ -52,37 +51,37 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { case "luck": Game1.player.LuckLevel = level; - Game1.player.experiencePoints[SFarmer.luckSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.luckSkill] = this.LevelExp[level]; monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info); break; case "mining": Game1.player.MiningLevel = level; - Game1.player.experiencePoints[SFarmer.miningSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.miningSkill] = this.LevelExp[level]; monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info); break; case "combat": Game1.player.CombatLevel = level; - Game1.player.experiencePoints[SFarmer.combatSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.combatSkill] = this.LevelExp[level]; monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info); break; case "farming": Game1.player.FarmingLevel = level; - Game1.player.experiencePoints[SFarmer.farmingSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.farmingSkill] = this.LevelExp[level]; monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info); break; case "fishing": Game1.player.FishingLevel = level; - Game1.player.experiencePoints[SFarmer.fishingSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.fishingSkill] = this.LevelExp[level]; monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info); break; case "foraging": Game1.player.ForagingLevel = level; - Game1.player.experiencePoints[SFarmer.foragingSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.foragingSkill] = this.LevelExp[level]; monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info); break; } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 89bf18ef..7578473b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -23,7 +23,6 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; -using SFarmer = StardewValley.Farmer; using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework @@ -872,10 +871,10 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); Game1.currentLocation.drawWater(Game1.spriteBatch); - IEnumerable source = Game1.currentLocation.farmers; + IEnumerable source = Game1.currentLocation.farmers; if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) - source = (IEnumerable)Game1.currentLocation.currentEvent.farmerActors; - IEnumerable farmers = source.Where((Func)(farmer => + source = (IEnumerable)Game1.currentLocation.currentEvent.farmerActors; + IEnumerable farmers = source.Where((Func)(farmer => { if (!farmer.IsLocalPlayer) return !(bool)((NetFieldBase)farmer.hidden); @@ -899,7 +898,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (SFarmer farmer in farmers) + foreach (Farmer farmer in farmers) { if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) { @@ -943,7 +942,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (SFarmer farmer in farmers) + foreach (Farmer farmer in farmers) { if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) { diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index f6bfc98e..032705b7 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -6,7 +6,6 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; -using SFarmer = StardewValley.Farmer; using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -34,7 +33,7 @@ namespace StardewModdingAPI.Framework.StateTracking ** Accessors *********/ /// The player being tracked. - public SFarmer Player { get; } + public Farmer Player { get; } /// The player's current location. public IValueWatcher LocationWatcher { get; } @@ -54,7 +53,7 @@ namespace StardewModdingAPI.Framework.StateTracking *********/ /// Construct an instance. /// The player to track. - public PlayerTracker(SFarmer player) + public PlayerTracker(Farmer player) { // init player data this.Player = player; -- cgit From a625e9bed71c6398a18ec0f5d41d7f8135660efd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 13:30:24 -0400 Subject: add initial multiplayer API (#480) --- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 6 ++++- .../Framework/ModHelpers/MultiplayerHelper.cs | 31 ++++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 3 +++ src/SMAPI/IModHelper.cs | 3 +++ src/SMAPI/IMultiplayerHelper.cs | 9 +++++++ src/SMAPI/Program.cs | 3 ++- src/SMAPI/StardewModdingAPI.csproj | 2 ++ 7 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs create mode 100644 src/SMAPI/IMultiplayerHelper.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index b5758d21..1f37a1be 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -45,6 +45,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for managing console commands. public ICommandHelper ConsoleCommands { get; } + /// Provides multiplayer utilities. + public IMultiplayerHelper Multiplayer { get; } + /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). public ITranslationHelper Translation { get; } @@ -66,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -82,6 +85,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); + this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); this.ContentPacks = contentPacks.ToArray(); this.CreateContentPack = createContentPack; diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs new file mode 100644 index 00000000..7a8da1d0 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -0,0 +1,31 @@ +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides multiplayer utilities. + internal class MultiplayerHelper : BaseHelper, IMultiplayerHelper + { + /********* + ** Properties + *********/ + /// SMAPI's core multiplayer utility. + private readonly SMultiplayer Multiplayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// SMAPI's core multiplayer utility. + public MultiplayerHelper(string modID, SMultiplayer multiplayer) + : base(modID) + { + this.Multiplayer = multiplayer; + } + + /// Get a new multiplayer ID. + public long GetNewID() + { + return this.Multiplayer.getNewID(); + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 7578473b..b206879c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -126,6 +126,9 @@ namespace StardewModdingAPI.Framework /// SMAPI's content manager. public ContentCore ContentCore { get; private set; } + /// The game's core multiplayer utility. + public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; + /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index e9554fdc..5e39161d 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -21,6 +21,9 @@ namespace StardewModdingAPI /// Metadata about loaded mods. IModRegistry ModRegistry { get; } + /// Provides multiplayer utilities. + IMultiplayerHelper Multiplayer { get; } + /// An API for managing console commands. ICommandHelper ConsoleCommands { get; } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs new file mode 100644 index 00000000..ac00b970 --- /dev/null +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI +{ + /// Provides multiplayer utilities. + public interface IMultiplayerHelper : IModLinked + { + /// Get a new multiplayer ID. + long GetNewID(); + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2325e79a..eaeb5f1d 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -807,6 +807,7 @@ namespace StardewModdingAPI IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); + IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) @@ -817,7 +818,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // get mod instance diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f6a16e5f..e0125c9b 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -102,6 +102,7 @@ + @@ -151,6 +152,7 @@ + -- cgit From 2fcc4d92c4c29951d1c669ffe42ebbb78e9a23d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 14:28:48 -0400 Subject: fix released-button detection (#453) --- src/SMAPI/Framework/Input/SInputState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 62defa9f..1b224737 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -225,7 +225,7 @@ namespace StardewModdingAPI.Framework.Input activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true); // handle released keys - foreach (KeyValuePair prev in activeButtons) + foreach (KeyValuePair prev in previousStatuses) { if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) activeButtons[prev.Key] = InputStatus.Released; -- cgit From e1eca00c66464005512127a80012b861b5c7c22e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 15:10:54 -0400 Subject: fix net field analyzers not detecting implicit conversions via binary expressions (#471) --- .../NetFieldAnalyzer.cs | 59 +++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 0e9154a1..39b9abc1 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace StardewModdingAPI.ModBuildConfig.Analyzer @@ -173,14 +174,24 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.ConditionalAccessExpression ); + context.RegisterSyntaxNodeAction( + this.AnalyzeBinaryComparison, + SyntaxKind.EqualsExpression, + SyntaxKind.NotEqualsExpression, + SyntaxKind.GreaterThanExpression, + SyntaxKind.GreaterThanOrEqualExpression, + SyntaxKind.LessThanExpression, + SyntaxKind.LessThanOrEqualExpression + ); } /********* ** Private methods *********/ - /// Analyse a syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available. + /// Analyse a member access syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available. /// The analysis context. + /// Returns whether any warnings were added. private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) { try @@ -203,7 +214,53 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer // warn: implicit conversion if (this.IsInvalidConversion(memberType)) + { context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); + return; + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); + } + } + + /// Analyse a binary comparison syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available. + /// The analysis context. + /// Returns whether any warnings were added. + private void AnalyzeBinaryComparison(SyntaxNodeAnalysisContext context) + { + // NOTE: implicit conversion within an operand is detected by the member access checks. + // This method is only concerned with the conversion of each side's final value. + try + { + BinaryExpressionSyntax expression = (BinaryExpressionSyntax)context.Node; + foreach (var pair in new[] { Tuple.Create(expression.Left, expression.Right), Tuple.Create(expression.Right, expression.Left) }) + { + // get node info + ExpressionSyntax curExpression = pair.Item1; // the side of the comparison being examined + ExpressionSyntax otherExpression = pair.Item2; // the other side + TypeInfo curType = context.SemanticModel.GetTypeInfo(curExpression); + TypeInfo otherType = context.SemanticModel.GetTypeInfo(otherExpression); + if (!this.IsNetType(curType.ConvertedType)) + continue; + + // warn for comparison to null + // An expression like `building.indoors != null` will sometimes convert `building.indoors` to NetFieldBase instead of object before comparison. Haven't reproduced this in unit tests yet. + Optional otherValue = context.SemanticModel.GetConstantValue(otherExpression); + if (otherValue.HasValue && otherValue.Value == null) + { + context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), curExpression, curType.Type.Name, "null")); + break; + } + + // warn for implicit conversion + if (!this.IsNetType(otherType.ConvertedType)) + { + context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), curExpression, curType.Type.Name, curType.ConvertedType)); + break; + } + } } catch (Exception ex) { -- cgit From 6be4d5abe03932a7f6a638816c2206388dc18983 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 16:07:41 -0400 Subject: detect conversions due to explicit casts or 'x as y' expressions (#471) --- .../NetFieldAnalyzerTests.cs | 6 +- .../AnalyzerUtilities.cs | 37 ++++++- .../NetFieldAnalyzer.cs | 113 +++++++++++++-------- .../ObsoleteFieldAnalyzer.cs | 2 +- 4 files changed, 112 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index ab3f2c5e..6f8c8b9b 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -59,7 +59,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests /// The expression which should be reported. /// The source type name which should be reported. /// The target type name which should be reported. - [TestCase("Item item = null; if (item.netIntField < 42);", 22, "item.netIntField", "NetInt", "int")] + [TestCase("Item item = null; if (item.netIntField < 42);", 22, "item.netIntField", "NetInt", "int")] // ↓ implicit conversion [TestCase("Item item = null; if (item.netIntField <= 42);", 22, "item.netIntField", "NetInt", "int")] [TestCase("Item item = null; if (item.netIntField > 42);", 22, "item.netIntField", "NetInt", "int")] [TestCase("Item item = null; if (item.netIntField >= 42);", 22, "item.netIntField", "NetInt", "int")] @@ -79,7 +79,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests [TestCase("Item item = null; if (item.netRefField != null);", 22, "item.netRefField", "NetRef", "object")] [TestCase("Item item = null; if (item.netRefProperty == null);", 22, "item.netRefProperty", "NetRef", "object")] [TestCase("Item item = null; if (item.netRefProperty != null);", 22, "item.netRefProperty", "NetRef", "object")] - [TestCase("SObject obj = null; if (obj.netIntField != 42);", 24, "obj.netIntField", "NetInt", "int")] // ↓ same as above, but inherited from base class + [TestCase("SObject obj = null; if (obj.netIntField != 42);", 24, "obj.netIntField", "NetInt", "int")] // ↓ implicit conversion for parent field [TestCase("SObject obj = null; if (obj.netIntProperty != 42);", 24, "obj.netIntProperty", "NetInt", "int")] [TestCase("SObject obj = null; if (obj.netRefField == null);", 24, "obj.netRefField", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefField != null);", 24, "obj.netRefField", "NetRef", "object")] @@ -87,6 +87,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests [TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")] [TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type [TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")] + [TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetInt", "int")] // ↓ explicit conversion to invalid type + [TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")] public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType) { // arrange diff --git a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs index e0c0cd63..68b5001e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace StardewModdingAPI.ModBuildConfig.Analyzer @@ -10,6 +11,40 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /********* ** Public methods *********/ + /// Get the metadata for an explicit cast or 'x as y' expression. + /// The member access expression. + /// provides methods for asking semantic questions about syntax nodes. + /// The expression whose value is being converted. + /// The type being converted from. + /// The type being converted to. + /// Returns true if the node is a matched expression, else false. + public static bool TryGetCastOrAsInfo(SyntaxNode node, SemanticModel semanticModel, out ExpressionSyntax fromExpression, out TypeInfo fromType, out TypeInfo toType) + { + // (type)x + if (node is CastExpressionSyntax cast) + { + fromExpression = cast.Expression; + fromType = semanticModel.GetTypeInfo(fromExpression); + toType = semanticModel.GetTypeInfo(cast.Type); + return true; + } + + // x as y + if (node is BinaryExpressionSyntax binary && binary.Kind() == SyntaxKind.AsExpression) + { + fromExpression = binary.Left; + fromType = semanticModel.GetTypeInfo(fromExpression); + toType = semanticModel.GetTypeInfo(binary.Right); + return true; + } + + // invalid + fromExpression = null; + fromType = default(TypeInfo); + toType = default(TypeInfo); + return false; + } + /// Get the metadata for a member access expression. /// The member access expression. /// provides methods for asking semantic questions about syntax nodes. @@ -17,7 +52,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// The type of the accessed member. /// The name of the accessed member. /// Returns true if the node is a member access expression, else false. - public static bool GetMemberInfo(SyntaxNode node, SemanticModel semanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName) + public static bool TryGetMemberInfo(SyntaxNode node, SemanticModel semanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName) { // simple access if (node is MemberAccessExpressionSyntax memberAccess) diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 39b9abc1..d1e5c59a 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -125,28 +125,27 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer ["StardewValley.Tool::upgradeLevel"] = "UpgradeLevel" }; - /// Describes the diagnostic rule covered by the analyzer. - private readonly IDictionary Rules = new Dictionary - { - ["AvoidImplicitNetFieldCast"] = new DiagnosticDescriptor( - id: "AvoidImplicitNetFieldCast", - title: "Netcode types shouldn't be implicitly converted", - messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/avoid-implicit-net-field-cast for details.", - category: "SMAPI.CommonErrors", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/avoid-implicit-net-field-cast" - ), - ["AvoidNetField"] = new DiagnosticDescriptor( - id: "AvoidNetField", - title: "Avoid Netcode types when possible", - messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.", - category: "SMAPI.CommonErrors", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/avoid-net-field" - ) - }; + /// The diagnostic info for an implicit net field cast. + private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor( + id: "AvoidImplicitNetFieldCast", + title: "Netcode types shouldn't be implicitly converted", + messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/avoid-implicit-net-field-cast for details.", + category: "SMAPI.CommonErrors", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://smapi.io/buildmsg/avoid-implicit-net-field-cast" + ); + + /// The diagnostic info for an avoidable net field access. + private readonly DiagnosticDescriptor AvoidNetFieldRule = new DiagnosticDescriptor( + id: "AvoidNetField", + title: "Avoid Netcode types when possible", + messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.", + category: "SMAPI.CommonErrors", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://smapi.io/buildmsg/avoid-net-field" + ); /********* @@ -162,7 +161,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// Construct an instance. public NetFieldAnalyzer() { - this.SupportedDiagnostics = ImmutableArray.CreateRange(this.Rules.Values); + this.SupportedDiagnostics = ImmutableArray.CreateRange(new[] { this.AvoidNetFieldRule, this.AvoidImplicitNetFieldCastRule }); } /// Called once at session start to register actions in the analysis context. @@ -174,6 +173,11 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.ConditionalAccessExpression ); + context.RegisterSyntaxNodeAction( + this.AnalyzeCast, + SyntaxKind.CastExpression, + SyntaxKind.AsExpression + ); context.RegisterSyntaxNodeAction( this.AnalyzeBinaryComparison, SyntaxKind.EqualsExpression, @@ -189,15 +193,15 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /********* ** Private methods *********/ - /// Analyse a member access syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available. + /// Analyse a member access syntax node and add a diagnostic message if applicable. /// The analysis context. /// Returns whether any warnings were added. private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) { - try + this.HandleErrors(context.Node, () => { // get member access info - if (!AnalyzerUtilities.GetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) + if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) return; if (!this.IsNetType(memberType.Type)) return; @@ -207,32 +211,45 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer { if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{memberName}", out string suggestedPropertyName)) { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidNetField"], context.Node.GetLocation(), context.Node, memberType.Type.Name, suggestedPropertyName)); + context.ReportDiagnostic(Diagnostic.Create(this.AvoidNetFieldRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, suggestedPropertyName)); return; } } // warn: implicit conversion - if (this.IsInvalidConversion(memberType)) + if (this.IsInvalidConversion(memberType.Type, memberType.ConvertedType)) { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); + context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); return; } - } - catch (Exception ex) + }); + } + + /// Analyse an explicit cast or 'x as y' node and add a diagnostic message if applicable. + /// The analysis context. + /// Returns whether any warnings were added. + private void AnalyzeCast(SyntaxNodeAnalysisContext context) + { + // NOTE: implicit conversion within the expression is detected by the member access + // checks. This method is only concerned with the conversion of its final value. + this.HandleErrors(context.Node, () => { - throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); - } + if (AnalyzerUtilities.TryGetCastOrAsInfo(context.Node, context.SemanticModel, out ExpressionSyntax fromExpression, out TypeInfo fromType, out TypeInfo toType)) + { + if (this.IsInvalidConversion(fromType.ConvertedType, toType.Type)) + context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), fromExpression, fromType.ConvertedType.Name, toType.Type)); + } + }); } - /// Analyse a binary comparison syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available. + /// Analyse a binary comparison syntax node and add a diagnostic message if applicable. /// The analysis context. /// Returns whether any warnings were added. private void AnalyzeBinaryComparison(SyntaxNodeAnalysisContext context) { // NOTE: implicit conversion within an operand is detected by the member access checks. // This method is only concerned with the conversion of each side's final value. - try + this.HandleErrors(context.Node, () => { BinaryExpressionSyntax expression = (BinaryExpressionSyntax)context.Node; foreach (var pair in new[] { Tuple.Create(expression.Left, expression.Right), Tuple.Create(expression.Right, expression.Left) }) @@ -250,34 +267,46 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer Optional otherValue = context.SemanticModel.GetConstantValue(otherExpression); if (otherValue.HasValue && otherValue.Value == null) { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), curExpression, curType.Type.Name, "null")); + context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), curExpression, curType.Type.Name, "null")); break; } // warn for implicit conversion if (!this.IsNetType(otherType.ConvertedType)) { - context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidImplicitNetFieldCast"], context.Node.GetLocation(), curExpression, curType.Type.Name, curType.ConvertedType)); + context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), curExpression, curType.Type.Name, curType.ConvertedType)); break; } } + }); + } + + /// Handle exceptions raised while analyzing a node. + /// The node being analysed. + /// The callback to invoke. + private void HandleErrors(SyntaxNode node, Action action) + { + try + { + action(); } catch (Exception ex) { - throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); + throw new InvalidOperationException($"Failed processing expression: '{node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); } } /// Get whether a net field was converted in an error-prone way. - /// The member access type info. - private bool IsInvalidConversion(TypeInfo typeInfo) + /// The source type. + /// The target type. + private bool IsInvalidConversion(ITypeSymbol fromType, ITypeSymbol toType) { // no conversion - if (!this.IsNetType(typeInfo.Type) || this.IsNetType(typeInfo.ConvertedType)) + if (!this.IsNetType(fromType) || this.IsNetType(toType)) return false; // conversion to implemented interface is OK - if (typeInfo.Type.AllInterfaces.Contains(typeInfo.ConvertedType)) + if (fromType.AllInterfaces.Contains(toType)) return false; // avoid any other conversions diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index a770f47d..3d353e52 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -74,7 +74,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer try { // get reference info - if (!AnalyzerUtilities.GetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) + if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) return; // suggest replacement -- cgit From 6257fdf57def0f07a7970f9fb232879ed4c524f6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 22:39:29 -0400 Subject: update wiki links --- docs/README.md | 2 +- docs/release-notes.md | 8 ++++---- docs/technical-docs.md | 2 +- src/SMAPI.Installer/readme.txt | 10 ++-------- src/SMAPI.Web/Views/Index/Index.cshtml | 3 +-- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- 6 files changed, 10 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/docs/README.md b/docs/README.md index bdfc5c9d..3112c023 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@ Have questions? Come [chat on Discord](https://discord.gg/KCJHWhX) with SMAPI de modders! ### For players -* [Modding guides](https://stardewvalleywiki.com/Modding:Index#For_players) +* [Player guide](https://stardewvalleywiki.com/Modding:Player_Guide) ### For modders * [Modding documentation](https://stardewvalleywiki.com/Modding:Index) diff --git a/docs/release-notes.md b/docs/release-notes.md index 93855ba9..8064a34e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -354,8 +354,8 @@ For players: * Updated mod compatibility list. For modders: -* Added `SDate` utility for in-game date calculations (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)). -* Added support for minimum dependency versions in `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). +* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)). +* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). * Added more useful logging when loading mods. * Added a `ModID` property to all mod helpers for extension methods. * Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code. @@ -387,8 +387,8 @@ For players: * Updated mod compatibility list. For modders: -* You can now add dependencies to `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). -* You can now translate your mod (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)). +* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). +* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)). * You can now load unpacked `.tbin` files from your mod folder through the content API. * SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 9e1a49e7..52c3f96d 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -44,7 +44,7 @@ executed. This doesn't work in MonoDevelop on Linux, unfortunately. ### Preparing a release To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See -[crossplatforming info](http://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) +[crossplatforming info](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) on the wiki for the first-time setup. 1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a diff --git a/src/SMAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt index a03ad6a4..12053a8c 100644 --- a/src/SMAPI.Installer/readme.txt +++ b/src/SMAPI.Installer/readme.txt @@ -14,15 +14,9 @@ SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. -Install guide +Player's guide -------------------------------- -See http://stardewvalleywiki.com/Modding:Installing_SMAPI. - - -Need help? --------------------------------- -- FAQs: http://stardewvalleywiki.com/Modding:Player_FAQs -- Ask for help: https://discord.gg/kH55QXP +See https://stardewvalleywiki.com/Modding:Player_Guide. Manual install diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 8ae23a45..a5ceb8aa 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -20,8 +20,7 @@ { Download SMAPI @Model.BetaVersion.Version
for Stardew Valley 1.3 beta

} - Install guide
- FAQs
+ Player guide
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 2d1c1b44..d051026f 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -177,7 +177,7 @@ else if (Model.ParsedLog?.IsValid == false)

Upload log file

    -
  1. Find your SMAPI log file (not the console text).
  2. +
  3. Find your SMAPI log file (not the console text).
  4. Drag the file onto the textbox below (or paste the text in).
  5. Click Parse.
-- cgit From 46fe7a86a7020d9170ab5a05047a3f7a6cc4b3c0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Apr 2018 11:35:01 -0400 Subject: add a few more avoidable net fields (#471) --- src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs | 7 +++++++ src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index d1e5c59a..fad9251b 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -22,6 +22,13 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /// Maps net fields to their equivalent non-net properties where available. private readonly IDictionary NetFieldWrapperProperties = new Dictionary { + // AnimatedSprite + ["StardewValley.AnimatedSprite::currentAnimation"] = "CurrentAnimation", + ["StardewValley.AnimatedSprite::currentFrame"] = "CurrentFrame", + ["StardewValley.AnimatedSprite::sourceRect"] = "SourceRect", + ["StardewValley.AnimatedSprite::spriteHeight"] = "SpriteHeight", + ["StardewValley.AnimatedSprite::spriteWidth"] = "SpriteWidth", + // Character ["StardewValley.Character::currentLocationRef"] = "currentLocation", ["StardewValley.Character::facingDirection"] = "FacingDirection", diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 6bb7736c..fa26875b 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0-beta-20180426 + 2.1.0-beta-20180428 Build package for SMAPI mods Pathoschild Pathoschild -- cgit From 89e2b76a5aa545331c6e8dd708015e2dc6e5dfe6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Apr 2018 12:51:15 -0400 Subject: fix multiplayer error after player joins but before location is loaded --- src/SMAPI/Framework/SGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b206879c..a580d3eb 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -293,7 +293,7 @@ namespace StardewModdingAPI.Framework /********* ** Update context *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) + if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0 && Game1.currentLocation != null) { if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) this.AfterLoadTimer--; -- cgit From b1833d09a5b921a3b8a8c4214f538e316d00e604 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Apr 2018 17:34:42 -0400 Subject: log trace message if all mods are up-to-date Otherwise it's not apparently whether all mods are up-to-date, or it's still waiting for a server response. --- src/SMAPI/Program.cs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index eaeb5f1d..acff0545 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -646,6 +646,8 @@ namespace StardewModdingAPI foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); } + else + this.Monitor.Log(" All mods up to date.", LogLevel.Trace); } catch (Exception ex) { -- cgit From 2dcd88deb147759c3ce9be3ccaeaf0625bd6d4a5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Apr 2018 20:24:40 -0400 Subject: update compatibility list --- src/SMAPI/StardewModdingAPI.metadata.json | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 1be5d3b1..cae6e741 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -226,7 +226,8 @@ "Birthday Mail": { "ID": "KathrynHazuka.BirthdayMail", "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update - "Default | UpdateKey": "Nexus:276" + "Default | UpdateKey": "Nexus:276", + "MapRemoteVersions": { "1.3.1": "1.3" } // manifest not updated }, "Breed Like Rabbits": { @@ -347,6 +348,11 @@ "Default | UpdateKey": "Nexus:1664" }, + "Cobalt": { + "ID": "spacechase0.Cobalt", + "MapRemoteVersions": { "1.1.3": "1.1.2" } // not updated in manifest + }, + "Cold Weather Haley": { "ID": "LordXamon.ColdWeatherHaleyPRO", "Default | UpdateKey": "Nexus:1169", @@ -406,7 +412,8 @@ "Crafting Counter": { "ID": "lolpcgaming.CraftingCounter", - "Default | UpdateKey": "Nexus:1585" + "Default | UpdateKey": "Nexus:1585", + "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest }, "Current Location": { @@ -916,6 +923,11 @@ "Default | UpdateKey": "Nexus:521" }, + "Magic": { + "ID": "spacechase0.Magic", + "MapRemoteVersions": { "0.1.2": "0.1.1" } // not updated in manifest + }, + "Mail Framework": { "ID": "DIGUS.MailFrameworkMod", "Default | UpdateKey": "Nexus:1536" @@ -1154,7 +1166,7 @@ "ID": "jwdred.PointAndPlant", "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 "Default | UpdateKey": "Nexus:572", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated }, "Pony Weight Loss Program": { @@ -1277,7 +1289,8 @@ "Save Anywhere": { "ID": "Omegasis.SaveAnywhere", "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl - "Default | UpdateKey": "Nexus:444" // added in 2.6.1 + "Default | UpdateKey": "Nexus:444", // added in 2.6.1 + "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest }, "Save Backup": { @@ -1445,7 +1458,7 @@ "Solar Eclipse Event": { "ID": "KoihimeNakamura.SolarEclipseEvent", "Default | UpdateKey": "Nexus:897", - "MapLocalVersions": { "1.3-20170917": "1.3" } + "MapLocalVersions": { "1.3.1-20180131": "1.3.1" } }, "SpaceCore": { @@ -1481,8 +1494,8 @@ }, "Sprinting Mod": { - "FormerIDs": "{EntryDll: 'SprintingMod.dll'}", - "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest + "FormerIDs": "{EntryDll: 'SprintingMod.dll', Author: 'Patrick'}", + "MapLocalVersions": { "2.0": "3.0" }, // not updated in manifest "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, @@ -1561,7 +1574,7 @@ "Default | UpdateKey": "Nexus:691" }, - "Summit Rain Fix": { + "Summit Reborn": { "ID": "KoihimeNakamura.summitreborn", "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) -- cgit From fec6adf82d5eda99c6bf34a21b0e01adc2b10d22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Apr 2018 21:55:26 -0400 Subject: fix build error on Linux/Mac --- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index 5eeb7ef6..33557385 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -133,7 +132,7 @@ namespace StardewModdingAPI.Framework.Input rightThumbStick: this.RightStickPos, leftTrigger: this.LeftTrigger, rightTrigger: this.RightTrigger, - buttons: this.GetPressedButtons().ToArray() + buttons: this.GetBitmask(this.GetPressedButtons()) // MonoDevelop requires one bitmask here; don't specify multiple values ); } @@ -149,5 +148,15 @@ namespace StardewModdingAPI.Framework.Input yield return button; } } + + /// Get a bitmask representing the given buttons. + /// The buttons to represent. + private Buttons GetBitmask(IEnumerable buttons) + { + Buttons flag = 0; + foreach (Buttons button in buttons) + flag |= button; + return flag; + } } } -- cgit From 6a3ff5f151e1d47033d86e7f9b1004ee405c525c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 Apr 2018 12:39:41 -0400 Subject: update for beta.2 --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index f89049c6..f680dc48 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0", + "Version": "2.6.0-beta.2", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 8641502f..e71f704e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.2"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.1"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.3"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 82ad4cef0ddca024db6df6d80358e54d965538dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 Apr 2018 21:46:05 -0400 Subject: fix input freeze on shipping screen (#482) --- src/SMAPI/Framework/SGame.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a580d3eb..d9e90896 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -210,6 +210,17 @@ namespace StardewModdingAPI.Framework { try { + /********* + ** Update input + *********/ + // This should *always* run, even when suppressing mod events, since the game uses + // this too. For example, doing this after mod event suppression would prevent the + // user from doing anything on the overnight shipping screen. + SInputState previousInputState = this.Input.Clone(); + SInputState inputState = this.Input; + if (this.IsActive) + inputState.TrueUpdate(); + /********* ** Skip conditions *********/ @@ -388,12 +399,8 @@ namespace StardewModdingAPI.Framework /********* ** Input events (if window has focus) *********/ - if (Game1.game1.IsActive) + if (this.IsActive) { - SInputState previousInputState = this.Input.Clone(); - SInputState inputState = this.Input; - inputState.TrueUpdate(); - // raise events bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); if (!isChatInput) -- cgit From 622140689042fd53b164e51a1872b9c2bdc71ed8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 Apr 2018 22:47:15 -0400 Subject: fix farmhand crash in some cases when host exits game --- src/SMAPI/Framework/SGame.cs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d9e90896..767b49c7 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -68,9 +68,6 @@ namespace StardewModdingAPI.Framework /// Whether the after-load events were raised for this session. private bool RaisedAfterLoadEvent; - /// Whether the game is returning to the menu. - private bool IsExitingToTitle; - /// Whether the game is saving and SMAPI has already raised . private bool IsBetweenSaveEvents; @@ -300,11 +297,12 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) this.OnGameInitialised(); - /********* ** Update context *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0 && Game1.currentLocation != null) + if (Context.IsWorldReady && !Context.IsSaveLoaded) + this.MarkWorldNotReady(); + else if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0 && Game1.currentLocation != null) { if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) this.AfterLoadTimer--; @@ -367,17 +365,9 @@ namespace StardewModdingAPI.Framework /********* ** Exit to title events *********/ - // before exit to title if (Game1.exitToTitle) - this.IsExitingToTitle = true; - - // after exit to title - if (Context.IsWorldReady && this.IsExitingToTitle && Game1.activeClickableMenu is TitleMenu) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); - - this.IsExitingToTitle = false; - this.CleanupAfterReturnToTitle(); this.Events.Save_AfterReturnToTitle.Raise(); } @@ -1242,8 +1232,8 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ - /// Perform any cleanup needed when the player unloads a save and returns to the title screen. - private void CleanupAfterReturnToTitle() + /// Perform any cleanup needed when a save is unloaded. + private void MarkWorldNotReady() { Context.IsWorldReady = false; this.AfterLoadTimer = 5; -- cgit From c6420f0e645687e13f3727e08adc4be6bafc7834 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 00:05:24 -0400 Subject: fix error during content manager disposal in some cases --- src/SMAPI/Framework/ContentCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index 6e915f28..80fedd6c 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -379,7 +379,7 @@ namespace StardewModdingAPI.Framework { foreach (var entry in this.ContentManagersByAssetKey) entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey[key].Any(), dispose: true); + this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey.TryGetValue(key, out var managers) || !managers.Any(), dispose: true); }); } -- cgit From 3255518f0a9d18f2d25859747a21bd54759b8f84 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 01:19:37 -0400 Subject: bump beta version --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index f680dc48..2198bf60 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.2", + "Version": "2.6.0-beta.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index e71f704e..9f2ebdb2 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.2"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.3"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.3"); -- cgit From 009a387526ee10b18d0ed3030d6e8868edf17203 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 18:44:39 -0400 Subject: unify SMAPI.AssemblyRewriters and SMAPI.Common projects --- build/common.targets | 2 +- build/prepare-install-package.targets | 4 +- docs/technical-docs.md | 6 +- .../Properties/AssemblyInfo.cs | 4 - src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs | 59 ------ .../StardewModdingAPI.AssemblyRewriters.csproj | 44 ----- src/SMAPI.Common/EnvironmentUtility.cs | 112 ------------ src/SMAPI.Common/Models/ModInfoModel.cs | 56 ------ src/SMAPI.Common/Models/ModSeachModel.cs | 37 ---- src/SMAPI.Common/Platform.cs | 15 -- src/SMAPI.Common/SemanticVersionImpl.cs | 199 --------------------- .../StardewModdingAPI.Common.projitems | 21 --- src/SMAPI.Common/StardewModdingAPI.Common.shproj | 13 -- src/SMAPI.Installer/InteractiveInstaller.cs | 5 +- .../StardewModdingAPI.Installer.csproj | 9 +- src/SMAPI.Internal/EnvironmentUtility.cs | 112 ++++++++++++ src/SMAPI.Internal/Models/ModInfoModel.cs | 56 ++++++ src/SMAPI.Internal/Models/ModSeachModel.cs | 37 ++++ src/SMAPI.Internal/Platform.cs | 15 ++ src/SMAPI.Internal/Properties/AssemblyInfo.cs | 9 + .../RewriteFacades/SpriteBatchMethods.cs | 59 ++++++ src/SMAPI.Internal/SemanticVersionImpl.cs | 199 +++++++++++++++++++++ .../StardewModdingAPI.Internal.csproj | 49 +++++ .../Framework/ModFileManager.cs | 2 +- .../StardewModdingAPI.ModBuildConfig.csproj | 7 +- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 2 +- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 2 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 4 +- src/SMAPI.sln | 12 +- src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 2 +- .../Framework/ModLoading/PlatformAssemblyMap.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/Framework/WebApiClient.cs | 2 +- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- src/SMAPI/Program.cs | 4 +- src/SMAPI/SemanticVersion.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 5 +- 46 files changed, 585 insertions(+), 605 deletions(-) delete mode 100644 src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs delete mode 100644 src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs delete mode 100644 src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj delete mode 100644 src/SMAPI.Common/EnvironmentUtility.cs delete mode 100644 src/SMAPI.Common/Models/ModInfoModel.cs delete mode 100644 src/SMAPI.Common/Models/ModSeachModel.cs delete mode 100644 src/SMAPI.Common/Platform.cs delete mode 100644 src/SMAPI.Common/SemanticVersionImpl.cs delete mode 100644 src/SMAPI.Common/StardewModdingAPI.Common.projitems delete mode 100644 src/SMAPI.Common/StardewModdingAPI.Common.shproj create mode 100644 src/SMAPI.Internal/EnvironmentUtility.cs create mode 100644 src/SMAPI.Internal/Models/ModInfoModel.cs create mode 100644 src/SMAPI.Internal/Models/ModSeachModel.cs create mode 100644 src/SMAPI.Internal/Platform.cs create mode 100644 src/SMAPI.Internal/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs create mode 100644 src/SMAPI.Internal/SemanticVersionImpl.cs create mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.csproj (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 588eea1b..54e24c74 100644 --- a/build/common.targets +++ b/build/common.targets @@ -98,7 +98,7 @@ - + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 8d10fc2e..8410f60e 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -29,7 +29,7 @@ - + @@ -43,7 +43,7 @@ - + diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 52c3f96d..a988eefc 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -78,8 +78,9 @@ on the wiki for the first-time setup. Mono.Cecil.dll Newtonsoft.Json.dll StardewModdingAPI - StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json + StardewModdingAPI.Internal.dll + StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml @@ -91,8 +92,9 @@ on the wiki for the first-time setup. Mods/* Mono.Cecil.dll Newtonsoft.Json.dll - StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json + StardewModdingAPI.Internal.dll + StardewModdingAPI.metadata.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml diff --git a/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs deleted file mode 100644 index f456a30d..00000000 --- a/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyTitle("SMAPI.AssemblyRewriters")] -[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")] diff --git a/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs deleted file mode 100644 index a7f100f2..00000000 --- a/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.AssemblyRewriters -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - public class SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } - - - /**** - ** MonoGame signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); - } - - /**** - ** XNA signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin() - { - base.Begin(); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState) - { - base.Begin(sortMode, blendState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); - } - } -} diff --git a/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj deleted file mode 100644 index 651b822d..00000000 --- a/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - Debug - x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49} - Library - Properties - StardewModdingAPI.AssemblyRewriters - StardewModdingAPI.AssemblyRewriters - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Common/EnvironmentUtility.cs b/src/SMAPI.Common/EnvironmentUtility.cs deleted file mode 100644 index 9d9e91e6..00000000 --- a/src/SMAPI.Common/EnvironmentUtility.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -#if SMAPI_FOR_WINDOWS -using System.Management; -#endif -using System.Runtime.InteropServices; - -namespace StardewModdingAPI.Common -{ - /// Provides methods for fetching environment information. - internal static class EnvironmentUtility - { - /********* - ** Properties - *********/ - /// Get the OS name from the system uname command. - /// The buffer to fill with the resulting string. - [DllImport("libc")] - static extern int uname(IntPtr buffer); - - - /********* - ** Public methods - *********/ - /// Detect the current OS. - public static Platform DetectPlatform() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - return Platform.Mac; - - case PlatformID.Unix: - return EnvironmentUtility.IsRunningMac() - ? Platform.Mac - : Platform.Linux; - - default: - return Platform.Windows; - } - } - - - /// Get the human-readable OS name and version. - /// The current platform. - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] - public static string GetFriendlyPlatformName(Platform platform) - { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; - } - - /// Get the name of the Stardew Valley executable. - /// The current platform. - public static string GetExecutableName(Platform platform) - { - return platform == Platform.Windows - ? "Stardew Valley.exe" - : "StardewValley.exe"; - } - - /// Get whether the platform uses Mono. - /// The current platform. - public static bool IsMono(this Platform platform) - { - return platform == Platform.Linux || platform == Platform.Mac; - } - - /********* - ** Private methods - *********/ - /// Detect whether the code is running on Mac. - /// - /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the - /// uname system command and checking the response, which is always 'Darwin' for MacOS. - /// - private static bool IsRunningMac() - { - IntPtr buffer = IntPtr.Zero; - try - { - buffer = Marshal.AllocHGlobal(8192); - if (uname(buffer) == 0) - { - string os = Marshal.PtrToStringAnsi(buffer); - return os == "Darwin"; - } - return false; - } - catch - { - return false; // default to Linux - } - finally - { - if (buffer != IntPtr.Zero) - Marshal.FreeHGlobal(buffer); - } - } - } -} diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs deleted file mode 100644 index 48df235a..00000000 --- a/src/SMAPI.Common/Models/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Common.Models -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The semantic version for the mod's latest release. - public string Version { get; set; } - - /// The semantic version for the mod's latest preview release, if available and different from . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/SMAPI.Common/Models/ModSeachModel.cs b/src/SMAPI.Common/Models/ModSeachModel.cs deleted file mode 100644 index 3c33d0b6..00000000 --- a/src/SMAPI.Common/Models/ModSeachModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Common.Models -{ - /// Specifies mods whose update-check info to fetch. - internal class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) - { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; - } - } -} diff --git a/src/SMAPI.Common/Platform.cs b/src/SMAPI.Common/Platform.cs deleted file mode 100644 index 08b4545f..00000000 --- a/src/SMAPI.Common/Platform.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Common -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux version of the game. - Linux, - - /// The Mac version of the game. - Mac, - - /// The Windows version of the game. - Windows - } -} diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs deleted file mode 100644 index 084f56a3..00000000 --- a/src/SMAPI.Common/SemanticVersionImpl.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI.Common -{ - /// A low-level implementation of a semantic version with an optional release tag. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). - internal class SemanticVersionImpl - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - public int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } - - /// An optional prerelease tag. - public string Tag { get; } - - /// 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*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; - - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - /********* - ** Public methods - *********/ - /// 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. - public SemanticVersionImpl(int major, int minor, int patch, string tag = null) - { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); - } - - /// Construct an instance. - /// The assembly version. - /// The is null. - public SemanticVersionImpl(Version version) - { - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version can't be null."); - - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; - } - - /// Construct an instance. - /// The semantic version string. - /// The is null. - /// The is not a valid semantic version. - public SemanticVersionImpl(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersionImpl.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. - public int CompareTo(SemanticVersionImpl other) - { - if (other == null) - throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); - } - - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// 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 prerelease tag to compare with this instance. - public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) - { - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); - bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); - string[] otherParts = otherTag.Split('.', '-'); - for (int i = 0; i < curParts.Length; i++) - { - // longer prerelease tag supercedes if otherwise equal - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - /// Get a string representation of the version. - public override string ToString() - { - // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; - - // tag - string tag = this.Tag; - if (tag != null) - result += $"-{tag}"; - return result; - } - - /// Parse a version string without throwing an exception if it fails. - /// The version string. - /// The parsed representation. - /// Returns whether parsing the version succeeded. - internal static bool TryParse(string version, out SemanticVersionImpl parsed) - { - try - { - parsed = new SemanticVersionImpl(version); - return true; - } - catch - { - parsed = null; - return false; - } - } - - - /********* - ** Private methods - *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) - { - tag = tag?.Trim(); - return !string.IsNullOrWhiteSpace(tag) ? tag : null; - } - } -} diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems deleted file mode 100644 index 0b89f092..00000000 --- a/src/SMAPI.Common/StardewModdingAPI.Common.projitems +++ /dev/null @@ -1,21 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - - - StardewModdingAPI.Common - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.shproj b/src/SMAPI.Common/StardewModdingAPI.Common.shproj deleted file mode 100644 index 0ef29144..00000000 --- a/src/SMAPI.Common/StardewModdingAPI.Common.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - 14.0 - - - - - - - - diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 0d602b57..c0bc8f2c 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -7,7 +7,7 @@ using System.Reflection; using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingApi.Installer { @@ -83,7 +83,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.config.json"); yield return GetInstallPath("StardewModdingAPI.data.json"); - yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); + yield return GetInstallPath("StardewModdingAPI.Internal.dll"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); @@ -102,6 +102,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 7a71bef9..4f849b9b 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI.Installer StardewModdingAPI.Installer - v4.0 + v4.5 512 true @@ -57,7 +57,12 @@ PreserveNewest - + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.Internal + + diff --git a/src/SMAPI.Internal/EnvironmentUtility.cs b/src/SMAPI.Internal/EnvironmentUtility.cs new file mode 100644 index 00000000..a3581898 --- /dev/null +++ b/src/SMAPI.Internal/EnvironmentUtility.cs @@ -0,0 +1,112 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if SMAPI_FOR_WINDOWS +using System.Management; +#endif +using System.Runtime.InteropServices; + +namespace StardewModdingAPI.Internal +{ + /// Provides methods for fetching environment information. + internal static class EnvironmentUtility + { + /********* + ** Properties + *********/ + /// Get the OS name from the system uname command. + /// The buffer to fill with the resulting string. + [DllImport("libc")] + static extern int uname(IntPtr buffer); + + + /********* + ** Public methods + *********/ + /// Detect the current OS. + public static Platform DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return Platform.Mac; + + case PlatformID.Unix: + return EnvironmentUtility.IsRunningMac() + ? Platform.Mac + : Platform.Linux; + + default: + return Platform.Windows; + } + } + + + /// Get the human-readable OS name and version. + /// The current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + public static string GetFriendlyPlatformName(Platform platform) + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; + } + + /// Get the name of the Stardew Valley executable. + /// The current platform. + public static string GetExecutableName(Platform platform) + { + return platform == Platform.Windows + ? "Stardew Valley.exe" + : "StardewValley.exe"; + } + + /// Get whether the platform uses Mono. + /// The current platform. + public static bool IsMono(this Platform platform) + { + return platform == Platform.Linux || platform == Platform.Mac; + } + + /********* + ** Private methods + *********/ + /// Detect whether the code is running on Mac. + /// + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the + /// uname system command and checking the response, which is always 'Darwin' for MacOS. + /// + private static bool IsRunningMac() + { + IntPtr buffer = IntPtr.Zero; + try + { + buffer = Marshal.AllocHGlobal(8192); + if (EnvironmentUtility.uname(buffer) == 0) + { + string os = Marshal.PtrToStringAnsi(buffer); + return os == "Darwin"; + } + return false; + } + catch + { + return false; // default to Linux + } + finally + { + if (buffer != IntPtr.Zero) + Marshal.FreeHGlobal(buffer); + } + } + } +} diff --git a/src/SMAPI.Internal/Models/ModInfoModel.cs b/src/SMAPI.Internal/Models/ModInfoModel.cs new file mode 100644 index 00000000..725c88bb --- /dev/null +++ b/src/SMAPI.Internal/Models/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Internal.Models +{ + /// Generic metadata about a mod. + internal class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The semantic version for the mod's latest release. + public string Version { get; set; } + + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI.Internal/Models/ModSeachModel.cs b/src/SMAPI.Internal/Models/ModSeachModel.cs new file mode 100644 index 00000000..fac72135 --- /dev/null +++ b/src/SMAPI.Internal/Models/ModSeachModel.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Internal.Models +{ + /// Specifies mods whose update-check info to fetch. + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + /// Whether to allow non-semantic versions, instead of returning an error for those. + public bool AllowInvalidVersions { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The namespaced mod keys to search. + /// Whether to allow non-semantic versions, instead of returning an error for those. + public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + { + this.ModKeys = modKeys.ToArray(); + this.AllowInvalidVersions = allowInvalidVersions; + } + } +} diff --git a/src/SMAPI.Internal/Platform.cs b/src/SMAPI.Internal/Platform.cs new file mode 100644 index 00000000..81ca5c1f --- /dev/null +++ b/src/SMAPI.Internal/Platform.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Internal +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux version of the game. + Linux, + + /// The Mac version of the game. + Mac, + + /// The Windows version of the game. + Windows + } +} diff --git a/src/SMAPI.Internal/Properties/AssemblyInfo.cs b/src/SMAPI.Internal/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b314b353 --- /dev/null +++ b/src/SMAPI.Internal/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("SMAPI.Internal")] +[assembly: AssemblyDescription("Contains internal SMAPI code that's shared between its projects.")] +[assembly: InternalsVisibleTo("StardewModdingAPI")] +[assembly: InternalsVisibleTo("StardewModdingAPI.ModBuildConfig")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Installer")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs new file mode 100644 index 00000000..5e5d117e --- /dev/null +++ b/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Internal.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin() + { + base.Begin(); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} diff --git a/src/SMAPI.Internal/SemanticVersionImpl.cs b/src/SMAPI.Internal/SemanticVersionImpl.cs new file mode 100644 index 00000000..6da16336 --- /dev/null +++ b/src/SMAPI.Internal/SemanticVersionImpl.cs @@ -0,0 +1,199 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Internal +{ + /// A low-level implementation of a semantic version with an optional release tag. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + internal class SemanticVersionImpl + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + public int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + public int Patch { get; } + + /// An optional prerelease tag. + public string Tag { get; } + + /// 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*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; + + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + /********* + ** Public methods + *********/ + /// 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. + public SemanticVersionImpl(int major, int minor, int patch, string tag = null) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.Tag = this.GetNormalisedTag(tag); + } + + /// Construct an instance. + /// The assembly version. + /// The is null. + public SemanticVersionImpl(Version version) + { + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version can't be null."); + + this.Major = version.Major; + this.Minor = version.Minor; + this.Patch = version.Build; + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersionImpl(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersionImpl.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.Major = int.Parse(match.Groups["major"].Value); + this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + public int CompareTo(SemanticVersionImpl other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// 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 prerelease tag to compare with this instance. + public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.Major != otherMajor) + return this.Major.CompareTo(otherMajor); + if (this.Minor != otherMinor) + return this.Minor.CompareTo(otherMinor); + if (this.Patch != otherPatch) + return this.Patch.CompareTo(otherPatch); + if (this.Tag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Tag.Split('.', '-'); + string[] otherParts = otherTag.Split('.', '-'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// Get a string representation of the version. + public override string ToString() + { + // version + string result = this.Patch != 0 + ? $"{this.Major}.{this.Minor}.{this.Patch}" + : $"{this.Major}.{this.Minor}"; + + // tag + string tag = this.Tag; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out SemanticVersionImpl parsed) + { + try + { + parsed = new SemanticVersionImpl(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + return !string.IsNullOrWhiteSpace(tag) ? tag : null; + } + } +} diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj new file mode 100644 index 00000000..6e7fa368 --- /dev/null +++ b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj @@ -0,0 +1,49 @@ + + + + + Debug + x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49} + Library + Properties + StardewModdingAPI.Internal + StardewModdingAPI.Internal + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 64262dc2..ba2e671d 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Script.Serialization; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.ModBuildConfig.Framework { diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 2e3ba356..02564409 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -55,7 +55,12 @@ - + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.Internal + + \ No newline at end of file diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 0464e50a..92b4f2c0 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 24517263..fc90d067 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index f49fb05c..b5603bd9 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index edb00454..bd27d624 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 3e5a4272..2782e2b9 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 59eb8cd1..b12b24e2 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 4496400c..79fe8f87 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 6411ad4c..87a87ab7 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index cffb1092..1502f5d8 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Routing.Constraints; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Web.Framework { diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index e2eee8a8..bc337e7e 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -22,6 +22,8 @@ - + + + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index d84ce589..ff953751 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -24,7 +24,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject @@ -32,8 +32,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SM EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Common", "SMAPI.Common\StardewModdingAPI.Common.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" ProjectSection(SolutionItems) = preProject ..\docs\CONTRIBUTING.md = ..\docs\CONTRIBUTING.md @@ -61,12 +59,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - SMAPI.Common\StardewModdingAPI.Common.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 - SMAPI.Common\StardewModdingAPI.Common.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 - SMAPI.Common\StardewModdingAPI.Common.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4 - SMAPI.Common\StardewModdingAPI.Common.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms @@ -177,9 +169,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {0CF97929-B0D0-4D73-B7BF-4FF7191035F9} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 9f2ebdb2..0116ac42 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Internal; using StardewValley; namespace StardewModdingAPI diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index d95de4fe..8851fc7d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; using StardewValley; namespace StardewModdingAPI.Framework.Content diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index feaee047..6c0e1c14 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Internal; using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index 9499b538..f0a28b4a 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index cc511ed4..73915824 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs index 7f0122cf..e33b2681 100644 --- a/src/SMAPI/Framework/WebApiClient.cs +++ b/src/SMAPI/Framework/WebApiClient.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Net; using Newtonsoft.Json; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 0b532a18..c7abfbef 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; +using StardewModdingAPI.Internal.RewriteFacades; using StardewValley; namespace StardewModdingAPI.Metadata diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index acff0545..6b7c1ad3 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -14,8 +14,6 @@ using System.Threading; using System.Windows.Forms; #endif using Newtonsoft.Json; -using StardewModdingAPI.Common; -using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Events; @@ -28,6 +26,8 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Models; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 4826c947..0f2a5cb0 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,6 @@ using System; using Newtonsoft.Json; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; namespace StardewModdingAPI { diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e0125c9b..a06056f9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -302,11 +302,10 @@ false - - + {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.AssemblyRewriters + StardewModdingAPI.Internal -- cgit From b1a24452eef782332d699ef8193c01e0da8ffa01 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 19:15:56 -0400 Subject: add public platform constant for mods --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 5 ++++- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/GamePlatform.cs | 17 +++++++++++++++++ src/SMAPI/Program.cs | 4 ++-- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI/GamePlatform.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8064a34e..cde9cbc2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. + * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 0116ac42..2ef26704 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -46,6 +46,9 @@ namespace StardewModdingAPI /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; + /// The target game platform. + public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform; + /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -92,7 +95,7 @@ namespace StardewModdingAPI internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// The target game platform. - internal static Platform TargetPlatform { get; } = EnvironmentUtility.DetectPlatform(); + internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform(); /********* diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 8851fc7d..c2818cdd 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.Content this.Cache = reflection.GetField>(contentManager, "loadedAssets").GetValue(); // get key normalisation logic - if (Constants.TargetPlatform == Platform.Windows) + if (Constants.Platform == Platform.Windows) { IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); this.NormaliseAssetNameForPlatform = path => method.Invoke(path); diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 73915824..8df2e59b 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -167,7 +167,7 @@ namespace StardewModdingAPI.Framework // auto detect color scheme if (colorScheme == MonitorColorScheme.AutoDetect) { - if (Constants.TargetPlatform == Platform.Mac) + if (Constants.Platform == Platform.Mac) colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white. else colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; diff --git a/src/SMAPI/GamePlatform.cs b/src/SMAPI/GamePlatform.cs new file mode 100644 index 00000000..3bd74462 --- /dev/null +++ b/src/SMAPI/GamePlatform.cs @@ -0,0 +1,17 @@ +using StardewModdingAPI.Internal; + +namespace StardewModdingAPI +{ + /// The game's platform version. + public enum GamePlatform + { + /// The Linux version of the game. + Linux = Platform.Linux, + + /// The Mac version of the game. + Mac = Platform.Mac, + + /// The Windows version of the game. + Windows = Platform.Windows + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6b7c1ad3..ba3e238d 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -172,7 +172,7 @@ namespace StardewModdingAPI try { // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.TargetPlatform)}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); @@ -741,7 +741,7 @@ namespace StardewModdingAPI ); // get assembly loaders - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a06056f9..31bfd6fd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -262,6 +262,7 @@ + -- cgit From dc47ff15c57b0ba2263ab3b94b49e9906620f648 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 May 2018 17:58:48 -0400 Subject: fix log parser not recognising content packs with no description (#481) --- docs/release-notes.md | 3 +++ src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index cde9cbc2..d13d9f8c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,6 +23,9 @@ * Added `player_add name`, which lets you add items to your inventory by name instead of ID. * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). +* For the log parser: + * Fixed issue parsing content packs with no description. + * For SMAPI developers: * Added more consistent crossplatform handling using a new `EnvironmentUtility`, including MacOS detection. * Added beta update channel to SMAPI, the web API, and home page. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index b5603bd9..163176fd 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing 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(@"^ (?.+) (?.+) by (?.+) \| for (?.+?) \| (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+) (?.+) by (?.+) \| for (?.+?)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /********* -- cgit From b3e8f957e27839a1392b63689e7daf03862540c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 May 2018 21:04:46 -0400 Subject: reorganise to avoid errors deploying web app, fix WMI error in Linux installer --- build/build.targets | 92 ++++++++++++++++ build/common.targets | 122 --------------------- build/constants.targets | 25 +++++ .../StardewModdingAPI.Installer.csproj | 3 +- .../RewriteFacades/SpriteBatchMethods.cs | 59 ---------- .../StardewModdingAPI.Internal.csproj | 5 +- .../StardewModdingAPI.ModBuildConfig.csproj | 1 + .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 3 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 3 +- src/SMAPI.sln | 3 +- .../Framework/RewriteFacades/SpriteBatchMethods.cs | 60 ++++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 4 +- 13 files changed, 193 insertions(+), 189 deletions(-) create mode 100644 build/build.targets delete mode 100644 build/common.targets create mode 100644 build/constants.targets delete mode 100644 src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs create mode 100644 src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs (limited to 'src') diff --git a/build/build.targets b/build/build.targets new file mode 100644 index 00000000..270fd95d --- /dev/null +++ b/build/build.targets @@ -0,0 +1,92 @@ + + + + + + + + False + + + False + + + False + + + False + + + + + $(GamePath)\Netcode.dll + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + diff --git a/build/common.targets b/build/common.targets deleted file mode 100644 index 54e24c74..00000000 --- a/build/common.targets +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - $(HOME)/.steam/steam/steamapps/common/Stardew Valley - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - - - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - - - - - - - False - - - False - - - False - - - False - - - - - $(GamePath)\Netcode.dll - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - $(DefineConstants);SMAPI_FOR_UNIX - - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - diff --git a/build/constants.targets b/build/constants.targets new file mode 100644 index 00000000..3e10462a --- /dev/null +++ b/build/constants.targets @@ -0,0 +1,25 @@ + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + $(HOME)/.steam/steam/steamapps/common/Stardew Valley + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 4f849b9b..dd349e09 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -64,6 +64,7 @@ - + + \ No newline at end of file diff --git a/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs deleted file mode 100644 index 5e5d117e..00000000 --- a/src/SMAPI.Internal/RewriteFacades/SpriteBatchMethods.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Internal.RewriteFacades -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - public class SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } - - - /**** - ** MonoGame signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); - } - - /**** - ** XNA signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin() - { - base.Begin(); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState) - { - base.Begin(sortMode, blendState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); - } - } -} diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj index 6e7fa368..a87dcb0a 100644 --- a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj +++ b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj @@ -31,6 +31,8 @@ + + @@ -41,9 +43,8 @@ - - + \ No newline at end of file diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 02564409..c6d25c19 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -62,5 +62,6 @@ + \ No newline at end of file diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index d1f72c6c..15c1c9a8 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -94,5 +94,6 @@ - + + \ No newline at end of file diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 0c793817..001b006c 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -78,7 +78,8 @@ - + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. diff --git a/src/SMAPI.sln b/src/SMAPI.sln index ff953751..282d8397 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -43,7 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" ProjectSection(SolutionItems) = preProject - ..\build\common.targets = ..\build\common.targets + ..\build\build.targets = ..\build\build.targets + ..\build\constants.targets = ..\build\constants.targets ..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets ..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets diff --git a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs new file mode 100644 index 00000000..5dd21b92 --- /dev/null +++ b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin() + { + base.Begin(); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index c7abfbef..1063ae71 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -4,7 +4,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; -using StardewModdingAPI.Internal.RewriteFacades; +using StardewModdingAPI.Framework.RewriteFacades; using StardewValley; namespace StardewModdingAPI.Metadata diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 31bfd6fd..ca6459fe 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -127,6 +127,7 @@ + @@ -313,5 +314,6 @@ - + + \ No newline at end of file -- cgit From 8425f53b7d6a840051b0f34c29f266ab3b5e7529 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 May 2018 21:18:13 -0400 Subject: mark CJB Cheats Menu incompatible due to friendship bugs --- src/SMAPI/StardewModdingAPI.metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index cae6e741..a253f20b 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -315,8 +315,8 @@ "CJB Cheats Menu": { "ID": "CJBok.CheatsMenu", "FormerIDs": "CJBCheatsMenu", // changed in 1.14 - "Default | UpdateKey": "Nexus:4", - "~1.17 | Status": "AssumeBroken" // broke in SDV 1.3 + "Default | UpdateKey": "Nexus:4", + "~1.18-beta | Status": "AssumeBroken" // broke in SDV 1.3, first beta causes significant friendship bugs }, "CJB Item Spawner": { -- cgit From ff55901025365a342e0794df86f6122603e614c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 May 2018 21:51:06 -0400 Subject: compile separate Linux/Mac installer to avoid WMI reference errors (#485) --- build/prepare-install-package.targets | 3 ++- src/SMAPI.Installer/InteractiveInstaller.cs | 12 ++++++++++-- src/SMAPI.Installer/unix-install.sh | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 8410f60e..d67d271f 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -18,12 +18,13 @@ - + + diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index c0bc8f2c..3ce71673 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -17,6 +17,9 @@ namespace StardewModdingApi.Installer /********* ** Properties *********/ + /// The name of the installer file in the package. + private readonly string InstallerFileName = "install.exe"; + /// The value that represents Windows 7. private readonly Version Windows7Version = new Version(6, 1); @@ -82,7 +85,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("Newtonsoft.Json.dll"); yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.config.json"); - yield return GetInstallPath("StardewModdingAPI.data.json"); + yield return GetInstallPath("StardewModdingAPI.metadata.json"); yield return GetInstallPath("StardewModdingAPI.Internal.dll"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); @@ -180,7 +183,9 @@ namespace StardewModdingApi.Installer } // get folders - DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.IsMono() ? "Mono" : "Windows")); + DirectoryInfo packageDir = platform.IsMono() + ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono + : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows")); DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); var paths = new { @@ -300,6 +305,9 @@ namespace StardewModdingApi.Installer this.PrintDebug("Adding SMAPI files..."); foreach (FileInfo sourceFile in packageDir.EnumerateFiles().Where(this.ShouldCopyFile)) { + if (sourceFile.Name == this.InstallerFileName) + continue; + string targetPath = Path.Combine(installDir.FullName, sourceFile.Name); this.InteractivelyDelete(targetPath); sourceFile.CopyTo(targetPath); diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh index a0bd9346..df02bb37 100644 --- a/src/SMAPI.Installer/unix-install.sh +++ b/src/SMAPI.Installer/unix-install.sh @@ -14,7 +14,7 @@ fi # validate Mono & run installer if $COMMAND mono >/dev/null 2>&1; then - mono install.exe + mono internal/Mono/install.exe else echo "Oops! Looks like Mono isn't installed. Please install Mono from http://mono-project.com, reboot, and run this installer again." read -- cgit From 4cabd2b2e7c8fbd39c1f18f24f5e52a56f960f10 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 May 2018 23:48:52 -0400 Subject: mark Horse Whistle 1.1.2-unofficial.1 incompatible due to lag issue fixed in unofficial.2 --- src/SMAPI/StardewModdingAPI.metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index a253f20b..9f229b04 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -798,7 +798,8 @@ "Horse Whistle (icepuente)": { "ID": "icepuente.HorseWhistle", - "Default | UpdateKey": "Nexus:1131" + "Default | UpdateKey": "Nexus:1131", + "~1.1.2-unofficial.1-pathoschild | Status": "AssumeBroken" // causes significant lag, fixed in unofficial.2 }, "Hunger (Yyeadude)": { -- cgit From 6a6001c7e6a03ff6fb0da992a87c6ef30d6bc952 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 May 2018 00:14:35 -0400 Subject: add semanticVersion.IsPrerelease() --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/SConfig.cs | 2 +- src/SMAPI/ISemanticVersion.cs | 3 +++ src/SMAPI/Program.cs | 2 +- src/SMAPI/SemanticVersion.cs | 6 ++++++ 5 files changed, 12 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index d13d9f8c..e913618a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * Added code analysis to mod build config package to flag common issues as warnings. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. + * Added `semanticVersion.IsPrerelease()` method. * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index be84a6b9..b732921f 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.Models public bool CheckForUpdates { get; set; } /// Whether to show beta versions as valid updates. - public bool UseBetaChannel { get; set; } = Constants.ApiVersion.Build != null; + public bool UseBetaChannel { get; set; } = Constants.ApiVersion.IsPrerelease(); /// SMAPI's GitHub project name, used to perform update checks. public string GitHubProjectName { get; set; } diff --git a/src/SMAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs index 0483c97b..961ef777 100644 --- a/src/SMAPI/ISemanticVersion.cs +++ b/src/SMAPI/ISemanticVersion.cs @@ -24,6 +24,9 @@ namespace StardewModdingAPI /********* ** Accessors *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + /// Get whether this version is older than the specified version. /// The version to compare with this instance. bool IsOlderThan(ISemanticVersion other); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ba3e238d..64520ccf 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -670,7 +670,7 @@ namespace StardewModdingAPI return newVersion != null && newVersion.IsNewerThan(currentVersion) - && (useBetaChannel || newVersion.Build == null); + && (useBetaChannel || !newVersion.IsPrerelease()); } /// Create a directory path if it doesn't exist. diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 0f2a5cb0..9db1cf14 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -55,6 +55,12 @@ namespace StardewModdingAPI public SemanticVersion(Version version) : this(new SemanticVersionImpl(version)) { } + /// Whether this is a pre-release version. + public bool IsPrerelease() + { + return !string.IsNullOrWhiteSpace(this.Build); + } + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. /// The value is null. -- cgit From 4bee6311c4f6bf654d8b9c54a53597347de65c4f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 May 2018 00:23:26 -0400 Subject: add prompt when in beta channel and a new version is found --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 3 +++ src/SMAPI/Program.cs | 28 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e913618a..551dd1d5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * For players: * Updated for Stardew Valley 1.3 (no longer compatible with earlier versions). * Added beta update channel. + * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 2ef26704..f87141b2 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -88,6 +88,9 @@ namespace StardewModdingAPI /// The file path which stores a fatal crash message for the next run. internal static string FatalCrashMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.crash.marker"); + /// The file path which stores the detected update version for the next run. + internal static string UpdateMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.update.marker"); + /// The full path to the folder containing mods. internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 64520ccf..ebe44cf7 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -241,6 +241,23 @@ namespace StardewModdingAPI return; } + // check update marker + if (File.Exists(Constants.UpdateMarker)) + { + string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); + if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) + { + if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) + { + this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); + this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); + this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); + Console.ReadKey(); + } + } + File.Delete(Constants.UpdateMarker); + } + // show details if game crashed during last session if (File.Exists(Constants.FatalCrashMarker)) { @@ -548,6 +565,7 @@ namespace StardewModdingAPI this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version + ISemanticVersion updateFound = null; try { ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; @@ -560,9 +578,15 @@ namespace StardewModdingAPI this.Monitor.Log($"Error: {response.Error}"); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) + { + updateFound = latestBeta; this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); + } else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) + { + updateFound = latestStable; this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); + } else this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } @@ -575,6 +599,10 @@ namespace StardewModdingAPI ); } + // show update message on next launch + if (updateFound != null) + File.WriteAllText(Constants.UpdateMarker, updateFound.ToString()); + // check mod versions if (mods.Any()) { -- cgit From 5088ecf8c8a04a028b6a9c45f907eae50894ffe6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 May 2018 00:34:36 -0400 Subject: bump beta version --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 2198bf60..55eb3e12 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.3", + "Version": "2.6.0-beta.4", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index f87141b2..6b5196b3 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.3"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.4"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.3"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.5"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 60040854a3b95ab220a42c087bda7f9011dda8ca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 May 2018 01:38:08 -0400 Subject: switch back to shared project due to installer issues --- build/build.targets | 92 --------------- build/common.targets | 125 +++++++++++++++++++++ build/constants.targets | 25 ----- .../StardewModdingAPI.Installer.csproj | 10 +- src/SMAPI.Internal/Properties/AssemblyInfo.cs | 9 -- src/SMAPI.Internal/SMAPI.Internal.projitems | 18 +++ .../StardewModdingAPI.Internal.csproj | 50 --------- .../StardewModdingAPI.Internal.shproj | 13 +++ .../StardewModdingAPI.ModBuildConfig.csproj | 9 +- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 3 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 4 +- src/SMAPI.sln | 23 ++-- src/SMAPI/StardewModdingAPI.csproj | 10 +- 13 files changed, 173 insertions(+), 218 deletions(-) delete mode 100644 build/build.targets create mode 100644 build/common.targets delete mode 100644 build/constants.targets delete mode 100644 src/SMAPI.Internal/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Internal/SMAPI.Internal.projitems delete mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.csproj create mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.shproj (limited to 'src') diff --git a/build/build.targets b/build/build.targets deleted file mode 100644 index 270fd95d..00000000 --- a/build/build.targets +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - False - - - False - - - False - - - False - - - - - $(GamePath)\Netcode.dll - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - diff --git a/build/common.targets b/build/common.targets new file mode 100644 index 00000000..44c2c071 --- /dev/null +++ b/build/common.targets @@ -0,0 +1,125 @@ + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + $(HOME)/.steam/steam/steamapps/common/Stardew Valley + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + $(GamePath)\Netcode.dll + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + diff --git a/build/constants.targets b/build/constants.targets deleted file mode 100644 index 3e10462a..00000000 --- a/build/constants.targets +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - $(HOME)/.steam/steam/steamapps/common/Stardew Valley - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index dd349e09..2ad7e82a 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -57,14 +57,8 @@ PreserveNewest - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.Internal - - + - - + \ No newline at end of file diff --git a/src/SMAPI.Internal/Properties/AssemblyInfo.cs b/src/SMAPI.Internal/Properties/AssemblyInfo.cs deleted file mode 100644 index b314b353..00000000 --- a/src/SMAPI.Internal/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -[assembly: AssemblyTitle("SMAPI.Internal")] -[assembly: AssemblyDescription("Contains internal SMAPI code that's shared between its projects.")] -[assembly: InternalsVisibleTo("StardewModdingAPI")] -[assembly: InternalsVisibleTo("StardewModdingAPI.ModBuildConfig")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Installer")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems new file mode 100644 index 00000000..764cb12e --- /dev/null +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -0,0 +1,18 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 85208f8d-6fd1-4531-be05-7142490f59fe + + + SMAPI.Internal + + + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj deleted file mode 100644 index a87dcb0a..00000000 --- a/src/SMAPI.Internal/StardewModdingAPI.Internal.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - - Debug - x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49} - Library - Properties - StardewModdingAPI.Internal - StardewModdingAPI.Internal - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj new file mode 100644 index 00000000..a098828a --- /dev/null +++ b/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj @@ -0,0 +1,13 @@ + + + + 85208f8d-6fd1-4531-be05-7142490f59fe + 14.0 + + + + + + + + diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index c6d25c19..e0ea876f 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -55,13 +55,8 @@ - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.Internal - - + - + \ No newline at end of file diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index 15c1c9a8..d1f72c6c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -94,6 +94,5 @@ - - + \ No newline at end of file diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index bc337e7e..e4678269 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -22,8 +22,6 @@ - - - + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 282d8397..c8760dcb 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -24,8 +24,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" @@ -43,8 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" ProjectSection(SolutionItems) = preProject - ..\build\build.targets = ..\build\build.targets - ..\build\constants.targets = ..\build\constants.targets + ..\build\common.targets = ..\build\common.targets ..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets ..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets @@ -59,7 +56,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" +EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 + SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 + SMAPI.Internal\SMAPI.Internal.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4 + SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms @@ -99,16 +104,6 @@ Global {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index ca6459fe..1822b134 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -304,16 +304,10 @@ false - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.Internal - - + - - + \ No newline at end of file -- cgit From b36566f08d15561557636737c39c8a5d57d74496 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 3 May 2018 01:38:39 -0400 Subject: bump beta version for release --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 6b5196b3..b86ec4e3 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.4"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.5"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.5"); -- cgit From c8ae8c2b0021401783ac38a80db7e676674f623a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 01:27:21 -0400 Subject: load game synchronously to fix asset loader/editor errors --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 551dd1d5..85776a06 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. + * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 767b49c7..78d07fbf 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -218,6 +218,20 @@ namespace StardewModdingAPI.Framework if (this.IsActive) inputState.TrueUpdate(); + /********* + ** Load game synchronously + *********/ + if (Game1.gameMode == Game1.loadingMode) + { + this.Monitor.Log("Running game loader...", LogLevel.Trace); + while (Game1.gameMode == Game1.loadingMode) + { + base.Update(gameTime); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); + } + this.Monitor.Log("Game loader OK.", LogLevel.Trace); + } + /********* ** Skip conditions *********/ -- cgit From 72941d977f684182eaee868ca037c9bdd5d1a0ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 12:02:31 -0400 Subject: update supporters list --- src/SMAPI.Web/Views/Index/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index a5ceb8aa..01b8955c 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -77,7 +77,7 @@ else acerbicon, ChefRude, jwdred, - OfficialPiAddict, + Kono Tyran, Robby LaFarge, and a few anonymous users for their ongoing support; you're awesome! 🏅

-- cgit From 05f81cb85f09c8e82ea125f520e4a264abfc3869 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 18:46:46 -0400 Subject: update net field list --- src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index fad9251b..e6766e61 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -114,7 +114,6 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer ["StardewValley.Object::netName"] = "name", ["StardewValley.Object::price"] = "Price", ["StardewValley.Object::quality"] = "Quality", - ["StardewValley.Object::scale"] = "Scale", ["StardewValley.Object::stack"] = "Stack", ["StardewValley.Object::tileLocation"] = "TileLocation", ["StardewValley.Object::type"] = "Type", -- cgit From 8051862c7bd2fe498657eef4bb102b5ca33390a6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 20:44:20 -0400 Subject: add LocationEvents.ObjectsChanged event --- docs/release-notes.md | 1 + .../Events/EventArgsLocationObjectsChanged.cs | 26 ++++++-- src/SMAPI/Events/LocationEvents.cs | 8 +++ src/SMAPI/Framework/Events/EventManager.cs | 6 ++ src/SMAPI/Framework/SGame.cs | 64 +++++++++++++++---- .../Framework/StateTracking/LocationTracker.cs | 71 ++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/LocationTracker.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 85776a06..558ed004 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. + * Added `LocationEvents.ObjectsChanged`, raised when an object is added/removed in any location. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index de6bd365..a6138ddb 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -1,28 +1,46 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Netcode; -using Object = StardewValley.Object; +using StardewValley; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Events { - /// Event arguments for a event. + /// Event arguments for a or event. public class EventArgsLocationObjectsChanged : EventArgs { /********* ** Accessors *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the list. + public IEnumerable> Added { get; } + + /// The objects removed from the list. + public IEnumerable> Removed { get; } + /// The current list of objects in the current location. - public IDictionary> NewObjects { get; } + [Obsolete("Use " + nameof(EventArgsLocationObjectsChanged.Added))] + public IDictionary> NewObjects { get; } /********* ** Public methods *********/ /// Construct an instance. + /// The location which changed. + /// The objects added to the list. + /// The objects removed from the list. /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged(IDictionary> newObjects) + public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed, IDictionary> newObjects) { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); this.NewObjects = newObjects; } } diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index 81d13e9f..3ed09136 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -31,12 +31,20 @@ namespace StardewModdingAPI.Events } /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + [Obsolete("Use " + nameof(LocationEvents) + "." + nameof(LocationEvents.ObjectsChanged) + " instead")] public static event EventHandler LocationObjectsChanged { add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); } + /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + public static event EventHandler ObjectsChanged + { + add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value); + remove => LocationEvents.EventManager.Location_ObjectsChanged.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 87ff760f..9030ba97 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; @@ -114,8 +115,12 @@ namespace StardewModdingAPI.Framework.Events public readonly ManagedEvent Location_LocationsChanged; /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + [Obsolete] public readonly ManagedEvent Location_LocationObjectsChanged; + /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + public readonly ManagedEvent Location_ObjectsChanged; + /**** ** MenuEvents ****/ @@ -239,6 +244,7 @@ namespace StardewModdingAPI.Framework.Events this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + this.Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 78d07fbf..d2ba85f8 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -93,7 +93,10 @@ namespace StardewModdingAPI.Framework private readonly IValueWatcher SaveIdWatcher; /// Tracks changes to the location list. - private readonly ICollectionWatcher LocationsWatcher; + private readonly ICollectionWatcher LocationListWatcher; + + /// Tracks changes to each location. + private readonly IDictionary LocationWatchers = new Dictionary(); /// Tracks changes to . private readonly IValueWatcher ActiveMenuWatcher; @@ -162,14 +165,14 @@ namespace StardewModdingAPI.Framework this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); - this.LocationsWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); + this.LocationListWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); this.Watchers.AddRange(new IWatcher[] { this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, this.ActiveMenuWatcher, - this.LocationsWatcher + this.LocationListWatcher }); } @@ -349,6 +352,22 @@ namespace StardewModdingAPI.Framework watcher.Update(); this.CurrentPlayerTracker?.Update(); + // update location watchers + if (this.LocationListWatcher.IsChanged) + { + foreach (GameLocation location in this.LocationListWatcher.Removed.Union(this.LocationListWatcher.Added)) + { + if (this.LocationWatchers.TryGetValue(location, out LocationTracker watcher)) + { + this.Watchers.Remove(watcher); + this.LocationWatchers.Remove(location); + watcher.Dispose(); + } + } + foreach (GameLocation location in this.LocationListWatcher.Added) + this.LocationWatchers[location] = new LocationTracker(location); + } + /********* ** Locale changed events *********/ @@ -509,12 +528,12 @@ namespace StardewModdingAPI.Framework } // raise location list changed - if (this.LocationsWatcher.IsChanged) + if (this.LocationListWatcher.IsChanged) { if (this.VerboseLogging) { - string added = this.LocationsWatcher.Added.Any() ? string.Join(", ", this.LocationsWatcher.Added.Select(p => p.Name)) : "none"; - string removed = this.LocationsWatcher.Removed.Any() ? string.Join(", ", this.LocationsWatcher.Removed.Select(p => p.Name)) : "none"; + string added = this.LocationListWatcher.Added.Any() ? string.Join(", ", this.LocationListWatcher.Added.Select(p => p.Name)) : "none"; + string removed = this.LocationListWatcher.Removed.Any() ? string.Join(", ", this.LocationListWatcher.Removed.Select(p => p.Name)) : "none"; this.Monitor.Log($"Context: location list changed (added {added}; removed {removed}).", LogLevel.Trace); } @@ -524,6 +543,20 @@ namespace StardewModdingAPI.Framework // raise events that shouldn't be triggered on initial load if (!this.SaveIdWatcher.IsChanged) { + // raise location objects changed + foreach (LocationTracker watcher in this.LocationWatchers.Values) + { + if (watcher.LocationObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + var added = watcher.LocationObjectsWatcher.Added; + var removed = watcher.LocationObjectsWatcher.Removed; + watcher.Reset(); + + this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed, watcher.Location.netObjects.FieldDict)); + } + } + // raise player leveled up a skill foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) { @@ -542,12 +575,16 @@ namespace StardewModdingAPI.Framework } // raise current location's object list changed - if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) { - if (this.VerboseLogging) - this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); + if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher watcher)) + { + if (this.VerboseLogging) + this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); + + GameLocation location = curPlayer.GetCurrentLocation(); - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().netObjects.FieldDict)); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, watcher.Added, watcher.Removed, location.netObjects.FieldDict)); + } } // raise time changed @@ -570,9 +607,14 @@ namespace StardewModdingAPI.Framework // update state this.CurrentPlayerTracker?.Reset(); - this.LocationsWatcher.Reset(); + this.LocationListWatcher.Reset(); this.SaveIdWatcher.Reset(); this.TimeWatcher.Reset(); + if (!Context.IsWorldReady) + { + foreach (LocationTracker watcher in this.LocationWatchers.Values) + watcher.Reset(); + } /********* ** Game update diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs new file mode 100644 index 00000000..8cf4e7a2 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a location's data. + internal class LocationTracker : IWatcher + { + /********* + ** Properties + *********/ + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged => this.Watchers.Any(p => p.IsChanged); + + /// The tracked location. + public GameLocation Location { get; } + + /// Tracks changes to the current location's objects. + public IDictionaryWatcher LocationObjectsWatcher { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location to track. + public LocationTracker(GameLocation location) + { + this.Location = location; + + // init watchers + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.Watchers.AddRange(new[] + { + this.LocationObjectsWatcher + }); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + /// Update the current value if needed. + public void Update() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + } + + /// Set the current value as the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 1822b134..9f04887c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -148,6 +148,7 @@ + -- cgit From a65a49a62201cc897e73c265a0a808ef0baad002 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 20:54:15 -0400 Subject: fix install error on Linux/Mac in some cases --- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 558ed004..00ed6e9c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Added console warning for mods which don't have update checks configured. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed detection of GOG Galaxy install path in rare cases. + * Fixed install error on Linux/Mac in some cases. * For modders: * Added code analysis to mod build config package to flag common issues as warnings. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 3ce71673..2b9dd95e 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -233,7 +233,7 @@ namespace StardewModdingApi.Installer Console.ReadLine(); return; } - if (!this.HasXNA(platform)) + if (!this.HasXna(platform)) { this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup."); Console.ReadLine(); @@ -317,10 +317,13 @@ namespace StardewModdingApi.Installer if (platform.IsMono()) { this.PrintDebug("Safely replacing game launcher..."); - if (!File.Exists(paths.unixLauncherBackup)) - File.Move(paths.unixLauncher, paths.unixLauncherBackup); - else if (File.Exists(paths.unixLauncher)) - this.InteractivelyDelete(paths.unixLauncher); + if (File.Exists(paths.unixLauncher)) + { + if (!File.Exists(paths.unixLauncherBackup)) + File.Move(paths.unixLauncher, paths.unixLauncherBackup); + else + this.InteractivelyDelete(paths.unixLauncher); + } File.Move(paths.unixSmapiLauncher, paths.unixLauncher); } @@ -471,7 +474,7 @@ namespace StardewModdingApi.Installer /// Get whether the current system has XNA Framework installed. This only applies on Windows. /// The current platform. /// The current platform is not Windows. - private bool HasXNA(Platform platform) + private bool HasXna(Platform platform) { switch (platform) { @@ -514,8 +517,7 @@ namespace StardewModdingApi.Installer return; // delete children - var folder = entry as DirectoryInfo; - if (folder != null) + if (entry is DirectoryInfo folder) { foreach (FileSystemInfo child in folder.GetFileSystemInfos()) this.ForceDelete(child); -- cgit From b8fd3aedfe884741bdda8c68398427f875585456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 01:31:06 -0400 Subject: rewrite location events for multiplayer --- docs/release-notes.md | 12 +- .../Events/EventArgsCurrentLocationChanged.cs | 31 --- src/SMAPI/Events/EventArgsGameLocationsChanged.cs | 27 --- src/SMAPI/Events/EventArgsIntChanged.cs | 3 +- .../Events/EventArgsLocationBuildingsChanged.cs | 39 ++++ .../Events/EventArgsLocationObjectsChanged.cs | 18 +- src/SMAPI/Events/EventArgsLocationsChanged.cs | 33 +++ src/SMAPI/Events/EventArgsPlayerWarped.cs | 32 +++ src/SMAPI/Events/LocationEvents.cs | 20 +- src/SMAPI/Events/PlayerEvents.cs | 10 +- src/SMAPI/Framework/Events/EventManager.cs | 22 +- src/SMAPI/Framework/SGame.cs | 158 +++++++-------- .../FieldWatchers/NetCollectionWatcher.cs | 93 +++++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 8 + .../Framework/StateTracking/LocationTracker.cs | 21 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 32 --- .../StateTracking/WorldLocationsTracker.cs | 221 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +- 18 files changed, 566 insertions(+), 221 deletions(-) delete mode 100644 src/SMAPI/Events/EventArgsCurrentLocationChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsGameLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsPlayerWarped.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 00ed6e9c..ece388c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,7 +12,11 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. - * Added `LocationEvents.ObjectsChanged`, raised when an object is added/removed in any location. + * Replaced `LocationEvents` with a more powerful set of events for multiplayer: + * now raised for all locations; + * now includes added/removed building interiors; + * each event now provides a list of added/removed values; + * added buildings-changed event. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. @@ -21,8 +25,10 @@ * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. - * **Breaking change**: dropped some deprecated APIs. - * **Breaking change**: mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). + * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): + * dropped some deprecated APIs; + * `LocationEvents` have been rewritten (see above); + * mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). * In console commands: * Added `player_add name`, which lets you add items to your inventory by name instead of ID. diff --git a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs deleted file mode 100644 index 25d3ebf3..00000000 --- a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsCurrentLocationChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's current location. - public GameLocation NewLocation { get; } - - /// The player's previous location. - public GameLocation PriorLocation { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's previous location. - /// The player's current location. - public EventArgsCurrentLocationChanged(GameLocation priorLocation, GameLocation newLocation) - { - this.NewLocation = newLocation; - this.PriorLocation = priorLocation; - } - } -} diff --git a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs deleted file mode 100644 index 78ba38fa..00000000 --- a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsGameLocationsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The current list of game locations. - public IList NewLocations { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current list of game locations. - public EventArgsGameLocationsChanged(IList newLocations) - { - this.NewLocations = newLocations; - } - } -} diff --git a/src/SMAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs index 0c742d12..a018695c 100644 --- a/src/SMAPI/Events/EventArgsIntChanged.cs +++ b/src/SMAPI/Events/EventArgsIntChanged.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace StardewModdingAPI.Events { @@ -14,6 +14,7 @@ namespace StardewModdingAPI.Events /// The current value. public int NewInt { get; } + /********* ** Public methods *********/ diff --git a/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs new file mode 100644 index 00000000..e8184ebe --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationBuildingsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public EventArgsLocationBuildingsChanged(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index a6138ddb..410ef6e6 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -8,7 +8,7 @@ using SObject = StardewValley.Object; namespace StardewModdingAPI.Events { - /// Event arguments for a or event. + /// Event arguments for a event. public class EventArgsLocationObjectsChanged : EventArgs { /********* @@ -17,31 +17,25 @@ namespace StardewModdingAPI.Events /// The location which changed. public GameLocation Location { get; } - /// The objects added to the list. + /// The objects added to the location. public IEnumerable> Added { get; } - /// The objects removed from the list. + /// The objects removed from the location. public IEnumerable> Removed { get; } - /// The current list of objects in the current location. - [Obsolete("Use " + nameof(EventArgsLocationObjectsChanged.Added))] - public IDictionary> NewObjects { get; } - /********* ** Public methods *********/ /// Construct an instance. /// The location which changed. - /// The objects added to the list. - /// The objects removed from the list. - /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed, IDictionary> newObjects) + /// The objects added to the location. + /// The objects removed from the location. + public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed) { this.Location = location; this.Added = added.ToArray(); this.Removed = removed.ToArray(); - this.NewObjects = newObjects; } } } diff --git a/src/SMAPI/Events/EventArgsLocationsChanged.cs b/src/SMAPI/Events/EventArgsLocationsChanged.cs new file mode 100644 index 00000000..20984f45 --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationsChanged.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public EventArgsLocationsChanged(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsPlayerWarped.cs b/src/SMAPI/Events/EventArgsPlayerWarped.cs new file mode 100644 index 00000000..93026aea --- /dev/null +++ b/src/SMAPI/Events/EventArgsPlayerWarped.cs @@ -0,0 +1,32 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsPlayerWarped : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's previous location. + public GameLocation PriorLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's previous location. + /// The player's current location. + public EventArgsPlayerWarped(GameLocation priorLocation, GameLocation newLocation) + { + this.NewLocation = newLocation; + this.PriorLocation = priorLocation; + } + } +} diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index 3ed09136..ff75c619 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -16,29 +16,21 @@ namespace StardewModdingAPI.Events /********* ** Events *********/ - /// Raised after the player warps to a new location. - public static event EventHandler CurrentLocationChanged - { - add => LocationEvents.EventManager.Location_CurrentLocationChanged.Add(value); - remove => LocationEvents.EventManager.Location_CurrentLocationChanged.Remove(value); - } - /// Raised after a game location is added or removed. - public static event EventHandler LocationsChanged + public static event EventHandler LocationsChanged { add => LocationEvents.EventManager.Location_LocationsChanged.Add(value); remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value); } - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - [Obsolete("Use " + nameof(LocationEvents) + "." + nameof(LocationEvents.ObjectsChanged) + " instead")] - public static event EventHandler LocationObjectsChanged + /// Raised after buildings are added or removed in a location. + public static event EventHandler BuildingsChanged { - add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); - remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); + add => LocationEvents.EventManager.Location_BuildingsChanged.Add(value); + remove => LocationEvents.EventManager.Location_BuildingsChanged.Remove(value); } - /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + /// Raised after objects are added or removed in a location. public static event EventHandler ObjectsChanged { add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value); diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs index 84a7ff63..6e7050e3 100644 --- a/src/SMAPI/Events/PlayerEvents.cs +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -23,13 +23,21 @@ namespace StardewModdingAPI.Events remove => PlayerEvents.EventManager.Player_InventoryChanged.Remove(value); } - /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. public static event EventHandler LeveledUp { add => PlayerEvents.EventManager.Player_LeveledUp.Add(value); remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value); } + /// Raised after the player warps to a new location. + public static event EventHandler Warped + { + add => PlayerEvents.EventManager.Player_Warped.Add(value); + remove => PlayerEvents.EventManager.Player_Warped.Remove(value); + } + + /********* ** Public methods diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9030ba97..84036127 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -108,17 +108,13 @@ namespace StardewModdingAPI.Framework.Events /**** ** LocationEvents ****/ - /// Raised after the player warps to a new location. - public readonly ManagedEvent Location_CurrentLocationChanged; - /// Raised after a game location is added or removed. - public readonly ManagedEvent Location_LocationsChanged; + public readonly ManagedEvent Location_LocationsChanged; - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - [Obsolete] - public readonly ManagedEvent Location_LocationObjectsChanged; + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent Location_BuildingsChanged; - /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + /// Raised after objects are added or removed in a location. public readonly ManagedEvent Location_ObjectsChanged; /**** @@ -160,6 +156,10 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. public readonly ManagedEvent Player_LeveledUp; + /// Raised after the player warps to a new location. + public readonly ManagedEvent Player_Warped; + + /**** ** SaveEvents ****/ @@ -241,9 +241,8 @@ namespace StardewModdingAPI.Framework.Events this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); - this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); this.Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); @@ -258,6 +257,7 @@ namespace StardewModdingAPI.Framework.Events this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); this.Player_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + this.Player_Warped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d2ba85f8..c8c30834 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -23,7 +23,6 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -92,11 +91,8 @@ namespace StardewModdingAPI.Framework /// Tracks changes to the save ID. private readonly IValueWatcher SaveIdWatcher; - /// Tracks changes to the location list. - private readonly ICollectionWatcher LocationListWatcher; - - /// Tracks changes to each location. - private readonly IDictionary LocationWatchers = new Dictionary(); + /// Tracks changes to the game's locations. + private readonly WorldLocationsTracker LocationsWatcher; /// Tracks changes to . private readonly IValueWatcher ActiveMenuWatcher; @@ -165,14 +161,14 @@ namespace StardewModdingAPI.Framework this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); - this.LocationListWatcher = WatcherFactory.ForObservableCollection((ObservableCollection)Game1.locations); + this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations); this.Watchers.AddRange(new IWatcher[] { this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, this.ActiveMenuWatcher, - this.LocationListWatcher + this.LocationsWatcher }); } @@ -351,22 +347,7 @@ namespace StardewModdingAPI.Framework foreach (IWatcher watcher in this.Watchers) watcher.Update(); this.CurrentPlayerTracker?.Update(); - - // update location watchers - if (this.LocationListWatcher.IsChanged) - { - foreach (GameLocation location in this.LocationListWatcher.Removed.Union(this.LocationListWatcher.Added)) - { - if (this.LocationWatchers.TryGetValue(location, out LocationTracker watcher)) - { - this.Watchers.Remove(watcher); - this.LocationWatchers.Remove(location); - watcher.Dispose(); - } - } - foreach (GameLocation location in this.LocationListWatcher.Added) - this.LocationWatchers[location] = new LocationTracker(location); - } + this.LocationsWatcher.Update(); /********* ** Locale changed events @@ -516,45 +497,86 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { - // update player info - PlayerTracker curPlayer = this.CurrentPlayerTracker; + bool raiseWorldEvents = !this.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded - // raise current location changed - if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) + // raise location changes + if (this.LocationsWatcher.IsChanged) { - if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); - this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(curPlayer.LocationWatcher.PreviousValue, newLocation)); + // location list changes + if (this.LocationsWatcher.IsLocationListChanged) + { + GameLocation[] added = this.LocationsWatcher.Added.ToArray(); + GameLocation[] removed = this.LocationsWatcher.Removed.ToArray(); + this.LocationsWatcher.ResetLocationList(); + + if (this.VerboseLogging) + { + string addedText = this.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = this.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); + } + + this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); + } + + // raise location contents changed + if (raiseWorldEvents) + { + foreach (LocationTracker watcher in this.LocationsWatcher.Locations) + { + // objects changed + if (watcher.ObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + var added = watcher.ObjectsWatcher.Added; + var removed = watcher.ObjectsWatcher.Removed; + watcher.ObjectsWatcher.Reset(); + + this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + } + + // buildings changed + if (watcher.BuildingsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + var added = watcher.BuildingsWatcher.Added; + var removed = watcher.BuildingsWatcher.Removed; + watcher.BuildingsWatcher.Reset(); + + this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); + } + } + } + else + this.LocationsWatcher.Reset(); } - // raise location list changed - if (this.LocationListWatcher.IsChanged) + // raise time changed + if (raiseWorldEvents && this.TimeWatcher.IsChanged) { + int was = this.TimeWatcher.PreviousValue; + int now = this.TimeWatcher.CurrentValue; + this.TimeWatcher.Reset(); + if (this.VerboseLogging) - { - string added = this.LocationListWatcher.Added.Any() ? string.Join(", ", this.LocationListWatcher.Added.Select(p => p.Name)) : "none"; - string removed = this.LocationListWatcher.Removed.Any() ? string.Join(", ", this.LocationListWatcher.Removed.Select(p => p.Name)) : "none"; - this.Monitor.Log($"Context: location list changed (added {added}; removed {removed}).", LogLevel.Trace); - } + this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace); - this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } + else + this.TimeWatcher.Reset(); - // raise events that shouldn't be triggered on initial load - if (!this.SaveIdWatcher.IsChanged) + // raise player events + if (raiseWorldEvents) { - // raise location objects changed - foreach (LocationTracker watcher in this.LocationWatchers.Values) - { - if (watcher.LocationObjectsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - var added = watcher.LocationObjectsWatcher.Added; - var removed = watcher.LocationObjectsWatcher.Removed; - watcher.Reset(); + PlayerTracker curPlayer = this.CurrentPlayerTracker; - this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed, watcher.Location.netObjects.FieldDict)); - } + // raise current location changed + if (curPlayer.TryGetNewLocation(out GameLocation newLocation)) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + this.Events.Player_Warped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation)); } // raise player leveled up a skill @@ -574,27 +596,6 @@ namespace StardewModdingAPI.Framework this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); } - // raise current location's object list changed - { - if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher watcher)) - { - if (this.VerboseLogging) - this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); - - GameLocation location = curPlayer.GetCurrentLocation(); - - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, watcher.Added, watcher.Removed, location.netObjects.FieldDict)); - } - } - - // raise time changed - if (this.TimeWatcher.IsChanged) - { - if (this.VerboseLogging) - this.Monitor.Log($"Context: time changed from {this.TimeWatcher.PreviousValue} to {this.TimeWatcher.CurrentValue}.", LogLevel.Trace); - this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.TimeWatcher.PreviousValue, this.TimeWatcher.CurrentValue)); - } - // raise mine level changed if (curPlayer.TryGetNewMineLevel(out int mineLevel)) { @@ -603,18 +604,11 @@ namespace StardewModdingAPI.Framework this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel)); } } + this.CurrentPlayerTracker?.Reset(); } - // update state - this.CurrentPlayerTracker?.Reset(); - this.LocationListWatcher.Reset(); + // update save ID watcher this.SaveIdWatcher.Reset(); - this.TimeWatcher.Reset(); - if (!Context.IsWorldReady) - { - foreach (LocationTracker watcher in this.LocationWatchers.Values) - watcher.Reset(); - } /********* ** Game update diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs new file mode 100644 index 00000000..f92edb90 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a Netcode collection. + internal class NetCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + where TValue : INetObject + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetCollectionWatcher(NetCollection field) + { + this.Field = field; + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the collection. + /// The added value. + private void OnValueAdded(TValue value) + { + this.AddedImpl.Add(value); + } + + /// A callback invoked when an entry is removed from the collection. + /// The added value. + private void OnValueRemoved(TValue value) + { + this.RemovedImpl.Add(value); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index bf261bb5..a4982faa 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -36,6 +36,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new ObservableCollectionWatcher(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net collection. + public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : INetObject + { + return new NetCollectionWatcher(collection); + } + /// Get a watcher for a net dictionary. /// The dictionary key type. /// The dictionary value type. diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 8cf4e7a2..07570401 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; using Object = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -26,8 +29,11 @@ namespace StardewModdingAPI.Framework.StateTracking /// The tracked location. public GameLocation Location { get; } - /// Tracks changes to the current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; } + /// Tracks changes to the location's buildings. + public ICollectionWatcher BuildingsWatcher { get; } + + /// Tracks changes to the location's objects. + public IDictionaryWatcher ObjectsWatcher { get; } /********* @@ -40,10 +46,15 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); - this.Watchers.AddRange(new[] + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.BuildingsWatcher = location is BuildableGameLocation buildableLocation + ? WatcherFactory.ForNetCollection(buildableLocation.buildings) + : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + + this.Watchers.AddRange(new IWatcher[] { - this.LocationObjectsWatcher + this.BuildingsWatcher, + this.ObjectsWatcher }); } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 032705b7..dea2e30d 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Xna.Framework; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking { @@ -38,9 +36,6 @@ namespace StardewModdingAPI.Framework.StateTracking /// The player's current location. public IValueWatcher LocationWatcher { get; } - /// Tracks changes to the player's current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; private set; } - /// The player's current mine level. public IValueWatcher MineLevelWatcher { get; } @@ -61,7 +56,6 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { @@ -77,7 +71,6 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.AddRange(new IWatcher[] { this.LocationWatcher, - this.LocationObjectsWatcher, this.MineLevelWatcher }); this.Watchers.AddRange(this.SkillWatchers.Values); @@ -93,16 +86,6 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (IWatcher watcher in this.Watchers) watcher.Update(); - // replace location objects watcher - if (this.LocationWatcher.IsChanged) - { - this.Watchers.Remove(this.LocationObjectsWatcher); - this.LocationObjectsWatcher.Dispose(); - - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); - this.Watchers.Add(this.LocationObjectsWatcher); - } - // update inventory this.CurrentInventory = this.GetInventory(); } @@ -154,21 +137,6 @@ namespace StardewModdingAPI.Framework.StateTracking return this.LocationWatcher.IsChanged; } - /// Get object changes to the player's current location if they there as of the last reset. - /// The object change watcher. - /// Returns whether it changed. - public bool TryGetLocationChanges(out IDictionaryWatcher watcher) - { - if (this.LocationWatcher.IsChanged) - { - watcher = null; - return false; - } - - watcher = this.LocationObjectsWatcher; - return watcher.IsChanged; - } - /// Get the player's new mine level if it changed. /// The player's current mine level. /// Returns whether it changed. diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs new file mode 100644 index 00000000..d9090c08 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Detects changes to the game's locations. + internal class WorldLocationsTracker : IWatcher + { + /********* + ** Properties + *********/ + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationListWatcher; + + /// A lookup of the tracked locations. + private IDictionary LocationDict { get; } = new Dictionary(); + + /// A lookup of registered buildings and their indoor location. + private readonly IDictionary BuildingIndoors = new Dictionary(); + + + /********* + ** Accessors + *********/ + /// Whether locations were added or removed since the last reset. + public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any(); + + /// Whether any tracked location data changed since the last reset. + public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged); + + /// The tracked locations. + public IEnumerable Locations => this.LocationDict.Values; + + /// The locations removed since the last update. + public ICollection Added { get; } = new HashSet(); + + /// The locations added since the last update. + public ICollection Removed { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game's list of locations. + public WorldLocationsTracker(ObservableCollection locations) + { + this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); + } + + /// Update the current value if needed. + public void Update() + { + // detect location changes + if (this.LocationListWatcher.IsChanged) + { + this.Remove(this.LocationListWatcher.Removed); + this.Add(this.LocationListWatcher.Added); + } + + // detect building changes + foreach (LocationTracker watcher in this.Locations.ToArray()) + { + if (watcher.BuildingsWatcher.IsChanged) + { + this.Remove(watcher.BuildingsWatcher.Removed); + this.Add(watcher.BuildingsWatcher.Added); + } + } + + // detect building interior changed (e.g. construction completed) + foreach (KeyValuePair pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value))) + { + GameLocation oldIndoors = pair.Value; + GameLocation newIndoors = pair.Key.indoors.Value; + + if (oldIndoors != null) + this.Added.Add(oldIndoors); + if (newIndoors != null) + this.Removed.Add(newIndoors); + } + + // update watchers + foreach (IWatcher watcher in this.Locations) + watcher.Update(); + } + + /// Set the current location list as the baseline. + public void ResetLocationList() + { + this.Removed.Clear(); + this.Added.Clear(); + this.LocationListWatcher.Reset(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.ResetLocationList(); + foreach (IWatcher watcher in this.Locations) + watcher.Reset(); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + this.LocationListWatcher.Dispose(); + foreach (IWatcher watcher in this.Locations) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /**** + ** Enumerable wrappers + ****/ + /// Add the given buildings. + /// The buildings to add. + public void Add(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Add(building); + } + + /// Add the given locations. + /// The locations to add. + public void Add(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Add(location); + } + + /// Remove the given buildings. + /// The buildings to remove. + public void Remove(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Remove(building); + } + + /// Remove the given locations. + /// The locations to remove. + public void Remove(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Remove(location); + } + + /**** + ** Main add/remove logic + ****/ + /// Add the given building. + /// The building to add. + public void Add(Building building) + { + if (building == null) + return; + + GameLocation indoors = building.indoors.Value; + this.BuildingIndoors[building] = indoors; + this.Add(indoors); + } + + /// Add the given location. + /// The location to add. + public void Add(GameLocation location) + { + if (location == null) + return; + + // remove old location if needed + this.Remove(location); + + // track change + this.Added.Add(location); + + // add + this.LocationDict[location] = new LocationTracker(location); + if (location is BuildableGameLocation buildableLocation) + this.Add(buildableLocation.buildings); + } + + /// Remove the given building. + /// The building to remove. + public void Remove(Building building) + { + if (building == null) + return; + + this.BuildingIndoors.Remove(building); + this.Remove(building.indoors.Value); + } + + /// Remove the given location. + /// The location to remove. + public void Remove(GameLocation location) + { + if (location == null) + return; + + if (this.LocationDict.TryGetValue(location, out LocationTracker watcher)) + { + // track change + this.Removed.Add(location); + + // remove + this.LocationDict.Remove(location); + watcher.Dispose(); + if (location is BuildableGameLocation buildableLocation) + this.Remove(buildableLocation.buildings); + } + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9f04887c..54fe9385 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -142,12 +143,14 @@ + + @@ -175,8 +178,8 @@ - - + + -- cgit From 30bf40ab2b03600b66091a6cbd61515cf0721844 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 02:27:43 -0400 Subject: fix input suppression not working in some cases --- docs/release-notes.md | 1 + src/SMAPI/Framework/Input/SInputState.cs | 89 +++++++++++++++++--------------- src/SMAPI/Framework/SGame.cs | 1 + 3 files changed, 49 insertions(+), 42 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ece388c7..d1a78aaa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,6 +23,7 @@ * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. + * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 1b224737..5e8efa62 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -81,22 +81,16 @@ namespace StardewModdingAPI.Framework.Input Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); - // get suppressed states - GamePadState suppressedController = realController; - KeyboardState suppressedKeyboard = realKeyboard; - MouseState suppressedMouse = realMouse; - if (this.SuppressButtons.Count > 0) - this.UpdateSuppression(activeButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); - - // update + // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - this.SuppressedController = suppressedController; - this.SuppressedKeyboard = suppressedKeyboard; - this.SuppressedMouse = suppressedMouse; this.MousePosition = mousePosition; + + // update suppressed states + this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); + this.UpdateSuppression(); } catch (InvalidOperationException) { @@ -104,6 +98,20 @@ namespace StardewModdingAPI.Framework.Input } } + /// Apply input suppression to current input. + public void UpdateSuppression() + { + GamePadState suppressedController = this.RealController; + KeyboardState suppressedKeyboard = this.RealKeyboard; + MouseState suppressedMouse = this.RealMouse; + + this.SuppressGivenStates(this.ActiveButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + + this.SuppressedController = suppressedController; + this.SuppressedKeyboard = suppressedKeyboard; + this.SuppressedMouse = suppressedMouse; + } + /// Get the gamepad state visible to the game. [Obsolete("This method should only be called by the game itself.")] public override GamePadState GetGamePadState() @@ -145,71 +153,68 @@ namespace StardewModdingAPI.Framework.Input return buttons.Any(button => this.IsDown(button.ToSButton())); } - /// Apply input suppression for the given input states. + + /********* + ** Private methods + *********/ + /// Whether input should be suppressed in the current context. + private bool ShouldSuppressNow() + { + return Game1.chatBox != null && !Game1.chatBox.isActive(); + } + + /// Apply input suppression to the given input states. /// The current button states to check. /// The game's keyboard state for the current tick. /// The game's mouse state for the current tick. /// The game's controller state for the current tick. - public void UpdateSuppression(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + private void SuppressGivenStates(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) { - // stop suppressing buttons once released - if (this.SuppressButtons.Count != 0) - this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); if (this.SuppressButtons.Count == 0) return; // gather info - HashSet keyboardButtons = new HashSet(); - HashSet controllerButtons = new HashSet(); - HashSet mouseButtons = new HashSet(); + HashSet suppressKeys = new HashSet(); + HashSet suppressButtons = new HashSet(); + HashSet suppressMouse = new HashSet(); foreach (SButton button in this.SuppressButtons) { if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - mouseButtons.Add(button); + suppressMouse.Add(button); else if (button.TryGetKeyboard(out Keys key)) - keyboardButtons.Add(key); + suppressKeys.Add(key); else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) - controllerButtons.Add(button); + suppressButtons.Add(button); } // suppress keyboard keys - if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) - keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + if (keyboardState.GetPressedKeys().Any() && suppressKeys.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(suppressKeys).ToArray()); // suppress controller keys - if (gamePadState.IsConnected && controllerButtons.Any()) + if (gamePadState.IsConnected && suppressButtons.Any()) { GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); - builder.SuppressButtons(controllerButtons); + builder.SuppressButtons(suppressButtons); gamePadState = builder.ToGamePadState(); } // suppress mouse buttons - if (mouseButtons.Any()) + if (suppressMouse.Any()) { mouseState = new MouseState( x: mouseState.X, y: mouseState.Y, scrollWheel: mouseState.ScrollWheelValue, - leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, - middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, - rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, - xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, - xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + leftButton: suppressMouse.Contains(SButton.MouseLeft) ? ButtonState.Released : mouseState.LeftButton, + middleButton: suppressMouse.Contains(SButton.MouseMiddle) ? ButtonState.Released : mouseState.MiddleButton, + rightButton: suppressMouse.Contains(SButton.MouseRight) ? ButtonState.Released : mouseState.RightButton, + xButton1: suppressMouse.Contains(SButton.MouseX1) ? ButtonState.Released : mouseState.XButton1, + xButton2: suppressMouse.Contains(SButton.MouseX2) ? ButtonState.Released : mouseState.XButton2 ); } } - - /********* - ** Private methods - *********/ - /// Whether input should be suppressed in the current context. - private bool ShouldSuppressNow() - { - return Game1.chatBox != null && !Game1.chatBox.isActive(); - } - /// Get the status of all pressed or released buttons relative to their previous status. /// The previous button statuses. /// The keyboard state. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c8c30834..63f7f073 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -613,6 +613,7 @@ namespace StardewModdingAPI.Framework /********* ** Game update *********/ + this.Input.UpdateSuppression(); try { base.Update(gameTime); -- cgit From 5121ae7b4a0b97b9e227b67903d0c6c88f3a3982 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 22:18:52 -0400 Subject: enforce mod ID convention (only alphanumeric, hyphen, dot, and underscore) --- docs/release-notes.md | 7 ++++--- src/SMAPI/Framework/ModLoading/ModResolver.cs | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index d1a78aaa..e4ae720d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -27,9 +27,10 @@ * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): - * dropped some deprecated APIs; - * `LocationEvents` have been rewritten (see above); - * mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). + * Dropped some deprecated APIs. + * `LocationEvents` have been rewritten (see above). + * Mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). + * Mod IDs should only contain letters, numbers, hyphens, dots, and underscores (so they can be safely used in many contexts like URLs); this restriction is now enforced. * In console commands: * Added `player_add name`, which lets you add items to your inventory by name instead of ID. diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index a9896278..cec37fca 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; @@ -194,8 +195,15 @@ namespace StardewModdingAPI.Framework.ModLoading missingFields.Add(nameof(IManifest.UniqueID)); if (missingFields.Any()) + { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + continue; + } } + + // validate ID format + if (Regex.IsMatch(mod.Manifest.UniqueID, "[^a-z0-9_.-]", RegexOptions.IgnoreCase)) + mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } // validate IDs are unique -- cgit From efe94c26536ff4a346685da5d11252a9a9a5a0eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 22:19:31 -0400 Subject: update supporters list --- src/SMAPI.Web/Views/Index/Index.cshtml | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 01b8955c..347eebc7 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -77,6 +77,7 @@ else acerbicon, ChefRude, jwdred, + KNakamura, Kono Tyran, Robby LaFarge, and a few anonymous users for their ongoing support; you're awesome! 🏅 -- cgit From 591b1aca780b4460c7845534eb624697025b891e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 22:20:15 -0400 Subject: update fence asset propagation in SDV 1.3 --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index f057eebe..7ca0bd82 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -320,7 +320,7 @@ namespace StardewModdingAPI.Metadata return this.ReloadNpcSprites(content, key, monster: true); if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"), StringComparison.InvariantCultureIgnoreCase)) - return this.ReloadFenceTextures(key); + return this.ReloadFenceTextures(content, key); if (this.IsInFolder(key, "Portraits")) return this.ReloadNpcPortraits(content, key); @@ -412,9 +412,10 @@ namespace StardewModdingAPI.Metadata } /// Reload the sprites for a fence type. + /// The content manager through which to reload the asset. /// The asset key to reload. /// Returns whether any textures were reloaded. - private bool ReloadFenceTextures(string key) + private bool ReloadFenceTextures(LocalizedContentManager content, string key) { // get fence type if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType)) @@ -425,16 +426,16 @@ namespace StardewModdingAPI.Metadata ( from location in this.GetLocations() from fence in location.Objects.Values.OfType() - where fenceType == 1 - ? fence.isGate.Value - : fence.whichType.Value == fenceType + where + fence.whichType.Value == fenceType + || (fence.isGate.Value && fenceType == 1) // gates are hardcoded to draw fence type 1 select fence ) .ToArray(); // update fence textures foreach (Fence fence in fences) - fence.reloadSprite(); + this.Reflection.GetField>(fence, "fenceTexture").SetValue(new Lazy(fence.loadFenceTexture)); return true; } -- cgit From 418ff99ea3b4d06432182b147d0637cce5eb0bae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 May 2018 21:00:35 -0400 Subject: add GetActiveLocations to multiplayer API (#480) --- src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index 7a8da1d0..c449a51b 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using StardewValley; + namespace StardewModdingAPI.Framework.ModHelpers { /// Provides multiplayer utilities. @@ -22,6 +25,12 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Multiplayer = multiplayer; } + /// Get the locations which are being actively synced from the host. + public IEnumerable GetActiveLocations() + { + return this.Multiplayer.activeLocations(); + } + /// Get a new multiplayer ID. public long GetNewID() { -- cgit From 1827e9f0718da2e8a285d94b3d5e35b465247cdf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 May 2018 21:12:55 -0400 Subject: bump for beta release --- src/SMAPI/Constants.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index b86ec4e3..50ec059a 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.5"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.7"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.5"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.9"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From f3cc08a52679fbe21c86e6df7c5750c9df93525f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 May 2018 23:04:36 -0400 Subject: fix new events losing track of changed data --- src/SMAPI/Framework/SGame.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 63f7f073..70462559 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -528,8 +528,8 @@ namespace StardewModdingAPI.Framework if (watcher.ObjectsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.ObjectsWatcher.Added; - var removed = watcher.ObjectsWatcher.Removed; + var added = watcher.ObjectsWatcher.Added.ToArray(); + var removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); @@ -539,8 +539,8 @@ namespace StardewModdingAPI.Framework if (watcher.BuildingsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.BuildingsWatcher.Added; - var removed = watcher.BuildingsWatcher.Removed; + var added = watcher.BuildingsWatcher.Added.ToArray(); + var removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); -- cgit From 7f5f222be559e39374a3b87eee07ed334edfe318 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 May 2018 23:04:49 -0400 Subject: bump version for beta 8 release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 55eb3e12..96d32090 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.4", + "Version": "2.6.0-beta.8", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 50ec059a..e2d06228 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.7"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.8"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.9"); -- cgit From 52a0231defb5bc65a4b2431610998045e0c98da6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 May 2018 23:31:24 -0400 Subject: add error when running Windows installer on Linux/Mac --- src/SMAPI.Installer/InteractiveInstaller.cs | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 2b9dd95e..b4ed2c92 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -149,6 +149,15 @@ namespace StardewModdingApi.Installer Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.WriteLine(); +#if SMAPI_FOR_WINDOWS + if (platform == Platform.Linux || platform == Platform.Mac) + { + this.PrintError($"This is the installer for Windows. Run the 'install on {platform}.{(platform == Platform.Linux ? "sh" : "command")}' file instead."); + Console.ReadLine(); + return; + } +#endif + /**** ** read command-line arguments ****/ -- cgit From 61b023916eb92237b3b10b30b77792139de1097d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 9 May 2018 23:58:58 -0400 Subject: rewrite content logic to decentralise cache (#488) This is necessary due to changes in Stardew Valley 1.3, which now changes loaded assets and expects those changes to be persisted but not propagated to other content managers. --- src/SMAPI/Framework/ContentCoordinator.cs | 175 ++++++ src/SMAPI/Framework/ContentCore.cs | 797 ------------------------ src/SMAPI/Framework/ContentManagerShim.cs | 91 --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 38 +- src/SMAPI/Framework/SContentManager.cs | 617 ++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 30 +- src/SMAPI/Program.cs | 10 +- src/SMAPI/StardewModdingAPI.csproj | 4 +- 8 files changed, 841 insertions(+), 921 deletions(-) create mode 100644 src/SMAPI/Framework/ContentCoordinator.cs delete mode 100644 src/SMAPI/Framework/ContentCore.cs delete mode 100644 src/SMAPI/Framework/ContentManagerShim.cs create mode 100644 src/SMAPI/Framework/SContentManager.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs new file mode 100644 index 00000000..86ebc5c3 --- /dev/null +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Metadata; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// The central logic for creating content managers, invalidating caches, and propagating asset changes. + internal class ContentCoordinator : IDisposable + { + /********* + ** Properties + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Provides metadata for core game assets. + private readonly CoreAssetPropagator CoreAssets; + + /// Simplifies access to private code. + private readonly Reflector Reflection; + + /// The loaded content managers (including the ). + private readonly IList ContentManagers = new List(); + + + /********* + ** Accessors + *********/ + /// The primary content manager used for most assets. + public SContentManager MainContentManager { get; private set; } + + /// The current language as a constant. + public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; + + /// Interceptors which provide the initial versions of matching assets. + public IDictionary> Loaders { get; } = new Dictionary>(); + + /// Interceptors which edit matching assets after they're loaded. + public IDictionary> Editors { get; } = new Dictionary>(); + + /// The absolute path to the . + public string FullRootDirectory { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection) + { + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Reflection = reflection; + this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); + this.ContentManagers.Add( + this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, isModFolder: false) + ); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection); + } + + /// Get a new content manager which defers loading to the content core. + /// A name for the mod manager. Not guaranteed to be unique. + /// Whether this content manager is wrapped around a mod folder. + /// The root directory to search for content (or null. for the default) + public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null) + { + return new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, isModFolder); + } + + /// Get the current content locale. + public string GetLocale() => this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + + /// Convert an absolute file path into a appropriate asset name. + /// The absolute path to the file. + public string GetAssetNameFromFilePath(string absolutePath) => this.MainContentManager.GetAssetNameFromFilePath(absolutePath, ContentSource.GameContent); + + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns the invalidated asset names. + public IEnumerable InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return new string[0]; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + if (canEdit == null || canLoad == null) + throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen + + // invalidate matching keys + return this.InvalidateCache(asset => + { + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); + }); + } + + /// 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 the invalidated asset keys. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + string locale = this.GetLocale(); + return this.InvalidateCache((assetName, type) => + { + IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.NormaliseAssetName); + return predicate(info); + }); + } + + /// 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 the invalidated asset names. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + // invalidate cache + HashSet removedAssetNames = new HashSet(); + foreach (SContentManager contentManager in this.ContentManagers) + { + foreach (string name in contentManager.InvalidateCache(predicate, dispose)) + removedAssetNames.Add(name); + } + + // reload core game assets + int reloaded = 0; + foreach (string key in removedAssetNames) + { + if (this.CoreAssets.Propagate(this.MainContentManager, key)) // use an intercepted content manager + reloaded++; + } + + // report result + if (removedAssetNames.Any()) + this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + + return removedAssetNames; + } + + /// Dispose held resources. + public void Dispose() + { + if (this.MainContentManager == null) + return; // already disposed + + this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); + foreach (SContentManager contentManager in this.ContentManagers) + contentManager.Dispose(); + this.ContentManagers.Clear(); + this.MainContentManager = null; + } + } +} diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs deleted file mode 100644 index 80fedd6c..00000000 --- a/src/SMAPI/Framework/ContentCore.cs +++ /dev/null @@ -1,797 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Metadata; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A thread-safe content handler which loads assets with support for mod injection and editing. - /// - /// This is the centralised content logic which manages all game assets. The game and mods don't use this class - /// directly; instead they use one of several instances, which proxy requests to - /// this class. That ensures that when the game disposes one content manager, the others can continue unaffected. - /// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously. - /// - /// Note that assets in the cache have two identifiers: the asset name (like "bundles") and key (like "bundles.pt-BR"). - /// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset - /// keys, and the game and mods only know about asset names. The content manager handles resolving them. - /// - internal class ContentCore : IDisposable - { - /********* - ** Properties - *********/ - /// The underlying content manager. - private readonly LocalizedContentManager Content; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// The underlying asset cache. - private readonly ContentCache Cache; - - /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. - private readonly IDictionary IsLocalisableLookup; - - /// The language enum values indexed by locale code. - private readonly IDictionary LanguageCodes; - - /// Provides metadata for core game assets. - private readonly CoreAssetPropagator CoreAssets; - - /// 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> ContentManagersByAssetKey = new Dictionary>(); - - /// The path prefix for assets in mod folders. - private readonly string ModContentPrefix; - - /// A lock used to prevents concurrent changes to the cache while data is being read. - private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - - /********* - ** Accessors - *********/ - /// The current language as a constant. - public LocalizedContentManager.LanguageCode Language => this.Content.GetCurrentLanguage(); - - /// Interceptors which provide the initial versions of matching assets. - public IDictionary> Loaders { get; } = new Dictionary>(); - - /// Interceptors which edit matching assets after they're loaded. - public IDictionary> Editors { get; } = new Dictionary>(); - - /// The absolute path to the . - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.Content.RootDirectory); - - /********* - ** Public methods - *********/ - /**** - ** Constructor - ****/ - /// Construct an instance. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - /// Encapsulates monitoring and logging. - /// Simplifies access to private code. - public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection) - { - // init - this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture); - this.Cache = new ContentCache(this.Content, reflection); - this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); - - // get asset data - this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection); - this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); - this.IsLocalisableLookup = reflection.GetField>(this.Content, "_localizedAsset").GetValue(); - } - - /// Get a new content manager which defers loading to the content core. - /// The content manager's name for logs (if any). - /// The root directory to search for content (or null. for the default) - public ContentManagerShim CreateContentManager(string name, string rootDirectory = null) - { - return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture); - } - - /**** - ** Asset key/name handling - ****/ - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. - [Pure] - public string NormalisePathSeparators(string path) - { - return this.Cache.NormalisePathSeparators(path); - } - - /// Normalise an asset name so it's consistent with the underlying cache. - /// The asset key. - [Pure] - public string NormaliseAssetName(string assetName) - { - return this.Cache.NormaliseKey(assetName); - } - - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - public void AssertValidAssetKeyFormat(string key) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new ArgumentException("The asset key or local path contains invalid characters."); - } - - /// Convert an absolute file path into a appropriate asset name. - /// The absolute path to the file. - public string GetAssetNameFromFilePath(string absolutePath) - { -#if SMAPI_FOR_WINDOWS - // XNA doesn't allow absolute asset paths, so get a path relative to the content folder - return this.GetRelativePath(absolutePath); -#else - // MonoGame is weird about relative paths on Mac, but allows absolute paths - return absolutePath; -#endif - } - - /**** - ** Content loading - ****/ - /// Get the current content locale. - public string GetLocale() - { - return this.GetLocale(this.Content.GetCurrentLanguage()); - } - - /// The locale for a language. - /// The language. - public string GetLocale(LocalizedContentManager.LanguageCode language) - { - return this.Content.LanguageCodeString(language); - } - - /// Get whether the content manager has already loaded and cached the given asset. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool IsLoaded(string assetName) - { - assetName = this.Cache.NormaliseKey(assetName); - return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName)); - } - - /// Get the cached asset keys. - public IEnumerable GetAssetKeys() - { - return this.WithReadLock(() => - this.Cache.Keys - .Select(this.GetAssetName) - .Distinct() - ); - } - - /// Load an asset through the content pipeline. When loading a .png file, this must be called outside the game's draw loop. - /// The expected asset type. - /// The asset path relative to the content directory. - /// The content manager instance for which to load the asset. - /// The language code for which to load content. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - public T Load(string assetName, ContentManager instance, LocalizedContentManager.LanguageCode language) - { - // normalise asset key - this.AssertValidAssetKeyFormat(assetName); - assetName = this.NormaliseAssetName(assetName); - - // load game content - if (!assetName.StartsWith(this.ModContentPrefix)) - return this.LoadImpl(assetName, instance, language); - - // load mod content - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); - try - { - return this.WithWriteLock(() => - { - // try cache - if (this.IsLoaded(assetName)) - return this.LoadImpl(assetName, instance, language); - - // get file - FileInfo file = this.GetModFile(assetName); - if (!file.Exists) - throw GetContentError("the specified path doesn't exist."); - - // load content - switch (file.Extension.ToLower()) - { - // XNB file - case ".xnb": - return this.LoadImpl(assetName, instance, language); - - // unpacked map - case ".tbin": - throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); - - // unpacked image - case ".png": - // validate - if (typeof(T) != typeof(Texture2D)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - - // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - this.InjectWithoutLock(assetName, texture, instance); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); - } - }); - } - catch (Exception ex) when (!(ex is SContentLoadException)) - { - if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") - throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); - throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); - } - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - /// The content manager instance for which to load the asset. - public void Inject(string assetName, T value, ContentManager instance) - { - this.WithWriteLock(() => this.InjectWithoutLock(assetName, value, instance)); - } - - /**** - ** Cache invalidation - ****/ - /// Purge assets from the cache that match one of the interceptors. - /// The asset editors for which to purge matching assets. - /// The asset loaders for which to purge matching assets. - /// Returns whether any cache entries were invalidated. - public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) - { - if (!editors.Any() && !loaders.Any()) - return false; - - // get CanEdit/Load methods - MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); - MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); - if (canEdit == null || canLoad == null) - throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen - - // invalidate matching keys - return this.InvalidateCache(asset => - { - // check loaders - MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); - if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) - return true; - - // check editors - MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); - return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); - }); - } - - /// 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, bool dispose = false) - { - string locale = this.GetLocale(); - return this.InvalidateCache((assetName, type) => - { - IAssetInfo info = new AssetInfo(locale, assetName, type, this.NormaliseAssetName); - return predicate(info); - }); - } - - /// 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, bool dispose = false) - { - return this.WithWriteLock(() => - { - // invalidate matching keys - HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => - { - this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) - { - removeAssetNames.Add(assetName); - removeKeys.Add(key); - return true; - } - return false; - }); - - // update reference tracking - foreach (string key in removeKeys) - this.ContentManagersByAssetKey.Remove(key); - - // reload core game assets - int reloaded = 0; - foreach (string key in removeAssetNames) - { - if (this.CoreAssets.Propagate(Game1.content, key)) // use an intercepted content manager - reloaded++; - } - - // report result - if (removeKeys.Any()) - { - this.Monitor.Log($"Invalidated {removeAssetNames.Count} asset names: {string.Join(", ", removeKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); - return true; - } - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return false; - }); - } - - /**** - ** Disposal - ****/ - /// 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); - - this.WithWriteLock(() => - { - foreach (var entry in this.ContentManagersByAssetKey) - entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey.TryGetValue(key, out var managers) || !managers.Any(), dispose: true); - }); - } - - - /********* - ** Private methods - *********/ - /**** - ** Disposal - ****/ - /// Dispose held resources. - public void Dispose() - { - this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - this.Content.Dispose(); - } - - /**** - ** Asset name/key handling - ****/ - /// Get a directory or file path relative to the content root. - /// The target file path. - private string GetRelativePath(string targetPath) - { - return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath); - } - - /// Get the locale codes (like ja-JP) used in asset keys. - private IDictionary GetKeyLocales() - { - // create locale => code map - IDictionary map = new Dictionary(); - foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) - map[code] = this.Content.LanguageCodeString(code); - - return map; - } - - /// Get the asset name from a cache key. - /// The input cache key. - private string GetAssetName(string cacheKey) - { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; - } - - /// Parse a cache key into its component parts. - /// The input cache key. - /// The original asset name. - /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) - { - // handle localised key - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); - if (lastSepIndex >= 0) - { - string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.LanguageCodes.ContainsKey(suffix)) - { - assetName = cacheKey.Substring(0, lastSepIndex); - localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - return; - } - } - } - - // handle simple key - assetName = cacheKey; - localeCode = null; - } - - /**** - ** Cache handling - ****/ - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - // default English - if (this.Language == LocalizedContentManager.LanguageCode.en) - return this.Cache.ContainsKey(normalisedAssetName); - - // translated - if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) - return false; - return localisable - ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.Content.GetCurrentLanguage())}") - : this.Cache.ContainsKey(normalisedAssetName); - } - - /// 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.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) - hash = this.ContentManagersByAssetKey[key] = new HashSet(); - hash.Add(manager); - } - - /**** - ** Content loading - ****/ - /// Load an asset name without heuristics to support mod content. - /// 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. - /// The language code for which to load content. - private T LoadImpl(string assetName, ContentManager instance, LocalizedContentManager.LanguageCode language) - { - return this.WithWriteLock(() => - { - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - { - this.TrackAssetLoader(assetName, instance); - return this.Content.Load(assetName, language); - } - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = this.Content.Load(assetName, language); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => - { - string locale = this.GetLocale(language); - IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = - this.ApplyLoader(info) - ?? new AssetDataForObject(info, this.Content.Load(assetName, language), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } - - // update cache & return data - this.InjectWithoutLock(assetName, data, instance); - return data; - }); - } - - /// Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - /// The content manager instance for which to load the asset. - private void InjectWithoutLock(string assetName, T value, ContentManager instance) - { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - this.TrackAssetLoader(assetName, instance); - } - - /// Get a file from the mod folder. - /// The asset path relative to the content folder. - private FileInfo GetModFile(string path) - { - // try exact match - FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); - - // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") - { - FileInfo result = new FileInfo(file.FullName + ".xnb"); - if (result.Exists) - file = result; - } - - return file; - } - - /// Load the initial asset from the registered . - /// The basic asset metadata. - /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData ApplyLoader(IAssetInfo info) - { - // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) - .Where(entry => - { - try - { - return entry.Value.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); - return false; - } - }) - .ToArray(); - - // validate loaders - if (!loaders.Any()) - return null; - if (loaders.Length > 1) - { - string[] loaderNames = loaders.Select(p => p.Key.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; - T data; - try - { - data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return null; - } - - // validate asset - if (data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); - return null; - } - - // return matched asset - return new AssetDataForObject(info, data, this.NormaliseAssetName); - } - - /// Apply any to a loaded asset. - /// The asset type. - /// The basic asset metadata. - /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) - { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); - - // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) - { - // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; - try - { - if (!editor.CanEdit(info)) - continue; - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // try edit - object prevAsset = asset.Data; - try - { - editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // validate edit - if (asset.Data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - else if (!(asset.Data is T)) - { - mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - } - - // 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); - } - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } - - /**** - ** Concurrency logic - ****/ - /// Acquire a read lock which prevents concurrent writes to the cache while it's open. - /// The action's return value. - /// The action to perform. - private T WithReadLock(Func action) - { - try - { - this.Lock.EnterReadLock(); - return action(); - } - finally - { - this.Lock.ExitReadLock(); - } - } - - /// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open. - /// The action to perform. - private void WithWriteLock(Action action) - { - try - { - this.Lock.EnterWriteLock(); - action(); - } - finally - { - this.Lock.ExitWriteLock(); - } - } - - /// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open. - /// The action's return value. - /// The action to perform. - private T WithWriteLock(Func action) - { - try - { - this.Lock.EnterWriteLock(); - return action(); - } - finally - { - this.Lock.ExitWriteLock(); - } - } - } -} diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs deleted file mode 100644 index 66754fd7..00000000 --- a/src/SMAPI/Framework/ContentManagerShim.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Globalization; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A minimal content manager which defers to SMAPI's core content logic. - internal class ContentManagerShim : LocalizedContentManager - { - /********* - ** Properties - *********/ - /// SMAPI's core content logic. - private readonly ContentCore ContentCore; - - - /********* - ** Accessors - *********/ - /// The content manager's name for logs (if any). - public string Name { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's core content logic. - /// The content manager's name for logs (if any). - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture) - : base(serviceProvider, rootDirectory, currentCulture) - { - this.ContentCore = contentCore; - 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.Load(assetName, LocalizedContentManager.CurrentLanguageCode); - } - - /// 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 language code for which to load content. - public override T Load(string assetName, LanguageCode language) - { - return this.ContentCore.Load(assetName, this, language); - } - - /// Load the base asset without localisation. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T LoadBase(string assetName) - { - return this.Load(assetName, LanguageCode.en); - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - public void Inject(string assetName, T value) - { - this.ContentCore.Inject(assetName, value, this); - } - - /// Create a new content manager for temporary use. - public override LocalizedContentManager CreateTemporary() - { - return this.ContentCore.CreateContentManager("(temporary)"); - } - - - /********* - ** Protected methods - *********/ - /// Dispose held resources. - /// Whether the content manager is disposing (rather than finalising). - protected override void Dispose(bool disposing) - { - this.ContentCore.DisposeFor(this); - } - } -} diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index c7d4c39e..4a71f7e7 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -23,10 +23,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Properties *********/ /// SMAPI's core content logic. - private readonly ContentCore ContentCore; + private readonly ContentCoordinator ContentCore; /// The content manager for this mod. - private readonly ContentManagerShim ContentManager; + private readonly SContentManager ContentManager; /// The absolute path to the mod folder. private readonly string ModFolderPath; @@ -42,10 +42,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Accessors *********/ /// The game's current locale code (like pt-BR). - public string CurrentLocale => this.ContentCore.GetLocale(); + public string CurrentLocale => this.ContentManager.GetLocale(); /// The game's current locale as an enum value. - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentCore.Language; + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.Language; /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. /// Encapsulates monitoring and logging. - public ContentHelper(ContentCore contentCore, ContentManagerShim contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + public ContentHelper(ContentCoordinator contentCore, SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentCore = contentCore; @@ -96,7 +96,7 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentManager.Load(key); + return this.ContentCore.MainContentManager.Load(key); case ContentSource.ModFolder: // get file @@ -105,10 +105,10 @@ namespace StardewModdingAPI.Framework.ModHelpers throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path - string assetName = this.ContentCore.GetAssetNameFromFilePath(file.FullName); + string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.ModFolder); // try cache - if (this.ContentCore.IsLoaded(assetName)) + if (this.ContentManager.IsLoaded(assetName)) return this.ContentManager.Load(assetName); // fix map tilesheets @@ -146,7 +146,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [Pure] public string NormaliseAssetName(string assetName) { - return this.ContentCore.NormaliseAssetName(assetName); + return this.ContentManager.NormaliseAssetName(assetName); } /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. @@ -158,11 +158,11 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentCore.NormaliseAssetName(key); + return this.ContentManager.NormaliseAssetName(key); case ContentSource.ModFolder: FileInfo file = this.GetModFile(key); - return this.ContentCore.NormaliseAssetName(this.ContentCore.GetAssetNameFromFilePath(file.FullName)); + return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.GameContent)); default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); - return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)); + return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any(); } /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. @@ -186,7 +186,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache() { this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); - return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)).Any(); } /// Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. @@ -195,7 +195,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache(Func predicate) { this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); - return this.ContentCore.InvalidateCache(predicate); + return this.ContentCore.InvalidateCache(predicate).Any(); } /********* @@ -207,7 +207,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] private void AssertValidAssetKeyFormat(string key) { - this.ContentCore.AssertValidAssetKeyFormat(key); + this.ContentManager.AssertValidAssetKeyFormat(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } @@ -235,7 +235,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get map info if (!map.TileSheets.Any()) return; - mapKey = this.ContentCore.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + 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 @@ -251,7 +251,7 @@ namespace StardewModdingAPI.Framework.ModHelpers string seasonalImageSource = null; if (Game1.currentSeason != null) { - string filename = Path.GetFileName(imageSource); + string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null."); bool hasSeasonalPrefix = filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) @@ -341,7 +341,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetModFile(string path) { // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentCore.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); // try with default extension @@ -360,7 +360,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetContentFolderFile(string key) { // get file path - string path = Path.Combine(this.ContentCore.FullRootDirectory, key); + string path = Path.Combine(this.ContentManager.FullRootDirectory, key); if (!path.EndsWith(".xnb")) path += ".xnb"; diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs new file mode 100644 index 00000000..8f008041 --- /dev/null +++ b/src/SMAPI/Framework/SContentManager.cs @@ -0,0 +1,617 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// A minimal content manager which defers to SMAPI's core content logic. + internal class SContentManager : LocalizedContentManager + { + /********* + ** Properties + *********/ + /// The central coordinator which manages content managers. + private readonly ContentCoordinator Coordinator; + + /// The underlying asset cache. + private readonly ContentCache Cache; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalisableLookup; + + /// The language enum values indexed by locale code. + private readonly IDictionary LanguageCodes; + + /// 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(); + + /// The path prefix for assets in mod folders. + private readonly string ModContentPrefix; + + /// Interceptors which provide the initial versions of matching assets. + private IDictionary> Loaders => this.Coordinator.Loaders; + + /// Interceptors which edit matching assets after they're loaded. + private IDictionary> Editors => this.Coordinator.Editors; + + + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + public string Name { get; } + + /// Whether this content manager is wrapped around a mod folder. + public bool IsModFolder { get; } + + /// The current language as a constant. + public LocalizedContentManager.LanguageCode Language => this.GetCurrentLanguage(); + + /// The absolute path to the . + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// Whether this content manager is wrapped around a mod folder. + public SContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, bool isModFolder) + : base(serviceProvider, rootDirectory, currentCulture) + { + // init + this.Name = name; + this.IsModFolder = isModFolder; + this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); + this.Cache = new ContentCache(this, reflection); + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath, ContentSource.GameContent); + + // get asset data + this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + + } + + /// 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.Load(assetName, LocalizedContentManager.CurrentLanguageCode); + } + + /// 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 language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + // normalise asset key + this.AssertValidAssetKeyFormat(assetName); + assetName = this.NormaliseAssetName(assetName); + + // load game content + if (!this.IsModFolder && !assetName.StartsWith(this.ModContentPrefix)) + return this.LoadImpl(assetName, language); + + // load mod content + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); + try + { + // try cache + if (this.IsLoaded(assetName)) + return this.LoadImpl(assetName, language); + + // get file + FileInfo file = this.GetModFile(assetName); + if (!file.Exists) + throw GetContentError("the specified path doesn't exist."); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + return this.LoadImpl(assetName, language); + + // unpacked map + case ".tbin": + throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.Inject(assetName, texture); + return (T)(object)texture; + } + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") + throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); + throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); + } + } + + /// Load the base asset without localisation. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T LoadBase(string assetName) + { + return this.Load(assetName, LanguageCode.en); + } + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + public void Inject(string assetName, T value) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + return this.Coordinator.CreateContentManager("(temporary)", isModFolder: false); + } + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + return this.Cache.NormalisePathSeparators(path); + } + + /// Normalise an asset name so it's consistent with the underlying cache. + /// The asset key. + [Pure] + public string NormaliseAssetName(string assetName) + { + return this.Cache.NormaliseKey(assetName); + } + + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public void AssertValidAssetKeyFormat(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + } + + /// Convert an absolute file path into an appropriate asset name. + /// The absolute path to the file. + /// The folder to which to get a relative path. + public string GetAssetNameFromFilePath(string absolutePath, ContentSource relativeTo) + { +#if SMAPI_FOR_WINDOWS + // XNA doesn't allow absolute asset paths, so get a path relative to the source folder + string sourcePath = relativeTo == ContentSource.GameContent ? this.Coordinator.FullRootDirectory : this.FullRootDirectory; + return this.GetRelativePath(sourcePath, absolutePath); +#else + // MonoGame is weird about relative paths on Mac, but allows absolute paths + return absolutePath; +#endif + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetLocale(this.GetCurrentLanguage()); + } + + /// The locale for a language. + /// The language. + public string GetLocale(LocalizedContentManager.LanguageCode language) + { + return this.LanguageCodeString(language); + } + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public bool IsLoaded(string assetName) + { + assetName = this.Cache.NormaliseKey(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } + + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + return this.Cache.Keys + .Select(this.GetAssetName) + .Distinct(); + } + + /**** + ** Cache invalidation + ****/ + /// 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 the number of invalidated assets. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => + { + this.ParseCacheKey(key, out string assetName, out _); + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) + { + removeAssetNames.Add(assetName); + return true; + } + return false; + }); + + return removeAssetNames; + } + + + /********* + ** Private methods + *********/ + /**** + ** Asset name/key handling + ****/ + /// Get a directory or file path relative to the content root. + /// The source file path. + /// The target file path. + private string GetRelativePath(string sourcePath, string targetPath) + { + return PathUtilities.GetRelativePath(sourcePath, targetPath); + } + + /// Get the locale codes (like ja-JP) used in asset keys. + private IDictionary GetKeyLocales() + { + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset name. + /// The asset locale code (or null if not localised). + private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.LanguageCodes.ContainsKey(suffix)) + { + assetName = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetName = cacheKey; + localeCode = null; + } + + /**** + ** Cache handling + ****/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + private bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + // default English + if (this.Language == LocalizedContentManager.LanguageCode.en) + return this.Cache.ContainsKey(normalisedAssetName); + + // translated + if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) + return false; + return localisable + ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}") + : this.Cache.ContainsKey(normalisedAssetName); + } + + /**** + ** Content loading + ****/ + /// Load an asset name without heuristics to support mod content. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + private T LoadImpl(string assetName, LocalizedContentManager.LanguageCode language) + { + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) + return base.Load(assetName, language); + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName, language); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + string locale = this.GetLocale(language); + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = + this.ApplyLoader(info) + ?? new AssetDataForObject(info, base.Load(assetName, language), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Inject(assetName, data); + return data; + } + + /// Get a file from the mod folder. + /// The asset path relative to the content folder. + private FileInfo GetModFile(string path) + { + // try exact match + FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(file.FullName + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) + { + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Value.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); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Key.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; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.NormaliseAssetName); + } + + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); + + // edit asset + foreach (var entry in this.GetInterceptors(this.Editors)) + { + // check for match + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // try edit + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // validate edit + if (asset.Data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + else if (!(asset.Data is T)) + { + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + } + + // 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); + } + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 70462559..48a70688 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -115,12 +115,15 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; + /// Whether the next content manager requested by the game will be for . + private bool NextContentManagerIsMain; + /********* ** Accessors *********/ /// SMAPI's content manager. - public ContentCore ContentCore { get; private set; } + public ContentCoordinator ContentCore { get; private set; } /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; @@ -140,6 +143,10 @@ namespace StardewModdingAPI.Framework /// A callback to invoke when the game exits. internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) { + // check expectations + if (this.ContentCore == null) + throw new InvalidOperationException($"The game didn't initialise its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change."); + // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -150,8 +157,6 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; - if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case - this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); @@ -190,14 +195,25 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // NOTE: this method is called from the Game1 constructor, before the SGame constructor runs. - // Don't depend on anything being initialised at this point. + // Game1._temporaryContent initialising from SGame constructor + // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialised at this point. if (this.ContentCore == null) { - this.ContentCore = new ContentCore(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); + this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); SGame.MonitorDuringInitialisation = null; + this.NextContentManagerIsMain = true; + return this.ContentCore.CreateContentManager("Game1._temporaryContent", isModFolder: false); } - return this.ContentCore.CreateContentManager("(generated)", rootDirectory); + + // Game1.content initialising from LoadContent + if (this.NextContentManagerIsMain) + { + this.NextContentManagerIsMain = false; + return this.ContentCore.CreateContentManager("Game1.content", isModFolder: false, rootDirectory: rootDirectory); + } + + // any other content manager + return this.ContentCore.CreateContentManager("(generated)", isModFolder: false, rootDirectory: rootDirectory); } /// The method called when the game is updating its state. This happens roughly 60 times per second. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ebe44cf7..1612ff11 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -63,7 +63,7 @@ namespace StardewModdingAPI private SGame GameInstance; /// The underlying content manager. - private ContentCore ContentCore => this.GameInstance.ContentCore; + private ContentCoordinator ContentCore => this.GameInstance.ContentCore; /// Tracks the installed mods. /// This is initialised after the game starts. @@ -721,7 +721,7 @@ namespace StardewModdingAPI /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. /// Handles access to SMAPI's internal mod metadata list. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCore contentCore, ModDatabase modDatabase) + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) { this.Monitor.Log("Loading mods...", LogLevel.Trace); @@ -748,7 +748,7 @@ namespace StardewModdingAPI // load mod as content pack IManifest manifest = metadata.Manifest; IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); metadata.SetMod(contentPack, monitor); @@ -833,7 +833,7 @@ namespace StardewModdingAPI IModHelper modHelper; { ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); @@ -843,7 +843,7 @@ namespace StardewModdingAPI IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - ContentManagerShim packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", packDirPath); + SContentManager packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", isModFolder: true, rootDirectory: packDirPath); IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 54fe9385..320b97e7 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -122,7 +122,7 @@ - + @@ -220,7 +220,7 @@ - + -- cgit From 5a2755bfcc516345800a4e9b966a0aff435215b8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:01:24 -0400 Subject: fix multiplayer.GetActiveLocations not added to interface (#480) --- src/SMAPI/IMultiplayerHelper.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs index ac00b970..43a0ac95 100644 --- a/src/SMAPI/IMultiplayerHelper.cs +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using StardewValley; + namespace StardewModdingAPI { /// Provides multiplayer utilities. @@ -5,5 +8,8 @@ namespace StardewModdingAPI { /// Get a new multiplayer ID. long GetNewID(); + + /// Get the locations which are being actively synced from the host. + IEnumerable GetActiveLocations(); } } -- cgit From 02c02a55eeeb744108d6a8335f6203a95ea20626 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:47:20 -0400 Subject: generalise console color logic for reuse (#495) --- .../ConsoleWriting/ColorfulConsoleWriter.cs | 136 +++++++++++++++++++++ src/SMAPI.Internal/ConsoleWriting/LogLevel.cs | 27 ++++ .../ConsoleWriting/MonitorColorScheme.cs | 15 +++ src/SMAPI.Internal/SMAPI.Internal.projitems | 3 + .../Logging/ConsoleInterceptionManager.cs | 27 ---- src/SMAPI/Framework/Models/MonitorColorScheme.cs | 15 --- src/SMAPI/Framework/Models/SConfig.cs | 2 + src/SMAPI/Framework/Monitor.cs | 111 +++-------------- src/SMAPI/LogLevel.cs | 16 +-- src/SMAPI/StardewModdingAPI.csproj | 1 - 10 files changed, 210 insertions(+), 143 deletions(-) create mode 100644 src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs create mode 100644 src/SMAPI.Internal/ConsoleWriting/LogLevel.cs create mode 100644 src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs delete mode 100644 src/SMAPI/Framework/Models/MonitorColorScheme.cs (limited to 'src') diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs new file mode 100644 index 00000000..5f8fe271 --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// Provides a wrapper for writing color-coded text to the console. + internal class ColorfulConsoleWriter + { + /********* + ** Properties + *********/ + /// The console text color for each log level. + private readonly IDictionary Colors; + + /// Whether the current console supports color formatting. + private readonly bool SupportsColor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The target platform. + /// The console color scheme to use. + public ColorfulConsoleWriter(Platform platform, MonitorColorScheme colorScheme) + { + this.SupportsColor = this.TestColorSupport(); + this.Colors = this.GetConsoleColorScheme(platform, colorScheme); + } + + /// Write a message line to the log. + /// The message to log. + /// The log level. + public void WriteLine(string message, ConsoleLogLevel level) + { + if (this.SupportsColor) + { + if (level == ConsoleLogLevel.Critical) + { + Console.BackgroundColor = ConsoleColor.Red; + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(message); + Console.ResetColor(); + } + else + { + Console.ForegroundColor = this.Colors[level]; + Console.WriteLine(message); + Console.ResetColor(); + } + } + else + Console.WriteLine(message); + } + + + /********* + ** Private methods + *********/ + /// Test whether the current console supports color formatting. + private bool TestColorSupport() + { + try + { + Console.ForegroundColor = Console.ForegroundColor; + return true; + } + catch (Exception) + { + return false; // Mono bug + } + } + + /// Get the color scheme to use for the current console. + /// The target platform. + /// The console color scheme to use. + private IDictionary GetConsoleColorScheme(Platform platform, MonitorColorScheme colorScheme) + { + // auto detect color scheme + if (colorScheme == MonitorColorScheme.AutoDetect) + { + colorScheme = platform == Platform.Mac + ? MonitorColorScheme.LightBackground // MacOS doesn't provide console background color info, but it's usually white. + : ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; + } + + // get colors for scheme + switch (colorScheme) + { + case MonitorColorScheme.DarkBackground: + return new Dictionary + { + [ConsoleLogLevel.Trace] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Debug] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Info] = ConsoleColor.White, + [ConsoleLogLevel.Warn] = ConsoleColor.Yellow, + [ConsoleLogLevel.Error] = ConsoleColor.Red, + [ConsoleLogLevel.Alert] = ConsoleColor.Magenta + }; + + case MonitorColorScheme.LightBackground: + return new Dictionary + { + [ConsoleLogLevel.Trace] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Debug] = ConsoleColor.DarkGray, + [ConsoleLogLevel.Info] = ConsoleColor.Black, + [ConsoleLogLevel.Warn] = ConsoleColor.DarkYellow, + [ConsoleLogLevel.Error] = ConsoleColor.Red, + [ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta + }; + + default: + throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); + } + } + + /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. + /// The color to check. + private static bool IsDark(ConsoleColor color) + { + switch (color) + { + case ConsoleColor.Black: + case ConsoleColor.Blue: + case ConsoleColor.DarkBlue: + case ConsoleColor.DarkMagenta: // Powershell + case ConsoleColor.DarkRed: + case ConsoleColor.Red: + return true; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs b/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs new file mode 100644 index 00000000..85e69f51 --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// The log severity levels. + internal enum ConsoleLogLevel + { + /// Tracing info intended for developers. + Trace, + + /// Troubleshooting info that may be relevant to the player. + Debug, + + /// Info relevant to the player. This should be used judiciously. + Info, + + /// An issue the player should be aware of. This should be used rarely. + Warn, + + /// A message indicating something went wrong. + Error, + + /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. + Alert, + + /// A critical issue that generally signals an immediate end to the application. + Critical + } +} diff --git a/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs new file mode 100644 index 00000000..bccb56d7 --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// A monitor color scheme to use. + internal enum MonitorColorScheme + { + /// Choose a color scheme automatically. + AutoDetect, + + /// Use lighter text colors that look better on a black or dark background. + DarkBackground, + + /// Use darker text colors that look better on a white or light background. + LightBackground + } +} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 764cb12e..dadae4b0 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -9,9 +9,12 @@ SMAPI.Internal + + + diff --git a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs index b8f2c34e..c04bcd1a 100644 --- a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs @@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Logging /********* ** Accessors *********/ - /// Whether the current console supports color formatting. - public bool SupportsColor { get; } - /// The event raised when a message is written to the console directly. public event Action OnMessageIntercepted; @@ -32,9 +29,6 @@ namespace StardewModdingAPI.Framework.Logging this.Output = new InterceptingTextWriter(Console.Out); this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); Console.SetOut(this.Output); - - // test color support - this.SupportsColor = this.TestColorSupport(); } /// Get an exclusive lock and write to the console output without interception. @@ -61,26 +55,5 @@ namespace StardewModdingAPI.Framework.Logging Console.SetOut(this.Output.Out); this.Output.Dispose(); } - - - /********* - ** private methods - *********/ - /// Test whether the current console supports color formatting. - private bool TestColorSupport() - { - try - { - this.ExclusiveWriteWithoutInterception(() => - { - Console.ForegroundColor = Console.ForegroundColor; - }); - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } } } diff --git a/src/SMAPI/Framework/Models/MonitorColorScheme.cs b/src/SMAPI/Framework/Models/MonitorColorScheme.cs deleted file mode 100644 index d8289d08..00000000 --- a/src/SMAPI/Framework/Models/MonitorColorScheme.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A monitor color scheme to use. - internal enum MonitorColorScheme - { - /// Choose a color scheme automatically. - AutoDetect, - - /// Use lighter text colors that look better on a black or dark background. - DarkBackground, - - /// Use darker text colors that look better on a white or light background. - LightBackground - } -} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b732921f..e201e966 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + namespace StardewModdingAPI.Framework.Models { /// The SMAPI configuration settings. diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 8df2e59b..2812a9cc 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.ConsoleWriting; namespace StardewModdingAPI.Framework { @@ -17,8 +15,11 @@ namespace StardewModdingAPI.Framework /// The name of the module which logs messages using this instance. private readonly string Source; + /// Handles writing color-coded text to the console. + private readonly ColorfulConsoleWriter ConsoleWriter; + /// Manages access to the console output. - private readonly ConsoleInterceptionManager ConsoleManager; + private readonly ConsoleInterceptionManager ConsoleInterceptor; /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -26,9 +27,6 @@ namespace StardewModdingAPI.Framework /// The maximum length of the values. private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); - /// The console text color for each log level. - private readonly IDictionary Colors; - /// Propagates notification that SMAPI should exit. private readonly CancellationTokenSource ExitTokenSource; @@ -54,21 +52,21 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. - /// Manages access to the console output. + /// Intercepts access to the console output. /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. /// The console color scheme to use. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) + public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) { // validate if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); // initialise - this.Colors = Monitor.GetConsoleColorScheme(colorScheme); this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); - this.ConsoleManager = consoleManager; + this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme); + this.ConsoleInterceptor = consoleInterceptor; this.ExitTokenSource = exitTokenSource; } @@ -77,7 +75,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { - this.LogImpl(this.Source, message, level, this.Colors[level]); + this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. @@ -92,7 +90,7 @@ namespace StardewModdingAPI.Framework internal void Newline() { if (this.WriteToConsole) - this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); + this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine); this.LogFile.WriteLine(""); } @@ -101,7 +99,7 @@ namespace StardewModdingAPI.Framework internal void LogUserInput(string input) { // user input already appears in the console, so just need to write to file - string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); + string prefix = this.GenerateMessagePrefix(this.Source, (ConsoleLogLevel)LogLevel.Info); this.LogFile.WriteLine($"{prefix} $>{input}"); } @@ -113,16 +111,14 @@ namespace StardewModdingAPI.Framework /// The message to log. private void LogFatal(string message) { - this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); + this.LogImpl(this.Source, message, ConsoleLogLevel.Critical); } /// Write a message line to the log. /// The name of the mod logging the message. /// The message to log. /// The log level. - /// The console foreground color. - /// The console background color (or null to leave it as-is). - private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) + private void LogImpl(string source, string message, ConsoleLogLevel level) { // generate message string prefix = this.GenerateMessagePrefix(source, level); @@ -130,20 +126,11 @@ namespace StardewModdingAPI.Framework string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; // write to console - if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) + if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) { - this.ConsoleManager.ExclusiveWriteWithoutInterception(() => + this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() => { - if (this.ConsoleManager.SupportsColor) - { - if (background.HasValue) - Console.BackgroundColor = background.Value; - Console.ForegroundColor = color; - Console.WriteLine(consoleMessage); - Console.ResetColor(); - } - else - Console.WriteLine(consoleMessage); + this.ConsoleWriter.WriteLine(consoleMessage, level); }); } @@ -154,72 +141,10 @@ namespace StardewModdingAPI.Framework /// Generate a message prefix for the current time. /// The name of the mod logging the message. /// The log level. - private string GenerateMessagePrefix(string source, LogLevel level) + private string GenerateMessagePrefix(string source, ConsoleLogLevel level) { string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]"; } - - /// Get the color scheme to use for the current console. - /// The console color scheme to use. - private static IDictionary GetConsoleColorScheme(MonitorColorScheme colorScheme) - { - // auto detect color scheme - if (colorScheme == MonitorColorScheme.AutoDetect) - { - if (Constants.Platform == Platform.Mac) - colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white. - else - colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; - } - - // get colors for scheme - switch (colorScheme) - { - case MonitorColorScheme.DarkBackground: - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; - - case MonitorColorScheme.LightBackground: - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.Black, - [LogLevel.Warn] = ConsoleColor.DarkYellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.DarkMagenta - }; - - default: - throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); - } - } - - /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. - /// The color to check. - private static bool IsDark(ConsoleColor color) - { - switch (color) - { - case ConsoleColor.Black: - case ConsoleColor.Blue: - case ConsoleColor.DarkBlue: - case ConsoleColor.DarkMagenta: // Powershell - case ConsoleColor.DarkRed: - case ConsoleColor.Red: - return true; - - default: - return false; - } - } } } diff --git a/src/SMAPI/LogLevel.cs b/src/SMAPI/LogLevel.cs index 89647876..7987f82a 100644 --- a/src/SMAPI/LogLevel.cs +++ b/src/SMAPI/LogLevel.cs @@ -1,24 +1,26 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + namespace StardewModdingAPI { /// The log severity levels. public enum LogLevel { /// Tracing info intended for developers. - Trace, + Trace = ConsoleLogLevel.Trace, /// Troubleshooting info that may be relevant to the player. - Debug, + Debug = ConsoleLogLevel.Debug, /// Info relevant to the player. This should be used judiciously. - Info, + Info = ConsoleLogLevel.Info, /// An issue the player should be aware of. This should be used rarely. - Warn, + Warn = ConsoleLogLevel.Warn, /// A message indicating something went wrong. - Error, + Error = ConsoleLogLevel.Error, /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. - Alert + Alert = ConsoleLogLevel.Alert } -} \ No newline at end of file +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 320b97e7..01d5aaf1 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -125,7 +125,6 @@ - -- cgit From 995a6fcca4a64d414d93e442419b31c89a2ce20f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:49:29 -0400 Subject: use SMAPI's console color scheme logic in installer too (#495) --- src/SMAPI.Installer/InteractiveInstaller.cs | 78 +++++++--------------- .../ConsoleWriting/ColorfulConsoleWriter.cs | 6 +- src/SMAPI.Internal/ConsoleWriting/LogLevel.cs | 5 +- 3 files changed, 33 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index b4ed2c92..5249355b 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -8,6 +8,7 @@ using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.ConsoleWriting; namespace StardewModdingApi.Installer { @@ -114,13 +115,19 @@ namespace StardewModdingApi.Installer yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files } - /// Whether the current console supports color formatting. - private static readonly bool ConsoleSupportsColor = InteractiveInstaller.GetConsoleSupportsColor(); + /// Handles writing color-coded text to the console. + private readonly ColorfulConsoleWriter ConsoleWriter; /********* ** Public methods *********/ + /// Construct an instance. + public InteractiveInstaller() + { + this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.AutoDetect); + } + /// Run the install or uninstall script. /// The command line arguments. /// @@ -366,7 +373,7 @@ namespace StardewModdingApi.Installer } // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(platform, modsDir, packagedModsDir); + this.InteractivelyRemoveAppDataMods(modsDir, packagedModsDir); } Console.WriteLine(); Console.WriteLine(); @@ -378,20 +385,20 @@ namespace StardewModdingApi.Installer { if (action == ScriptAction.Install) { - this.PrintColor("SMAPI is installed! If you use Steam, set your launch options to enable achievements (see smapi.io/install):", ConsoleColor.DarkGreen); - this.PrintColor($" \"{Path.Combine(installDir.FullName, "StardewModdingAPI.exe")}\" %command%", ConsoleColor.DarkGreen); + this.PrintSuccess("SMAPI is installed! If you use Steam, set your launch options to enable achievements (see smapi.io/install):"); + this.PrintSuccess($" \"{Path.Combine(installDir.FullName, "StardewModdingAPI.exe")}\" %command%"); Console.WriteLine(); - this.PrintColor("If you don't use Steam, launch StardewModdingAPI.exe in your game folder to play with mods.", ConsoleColor.DarkGreen); + this.PrintSuccess("If you don't use Steam, launch StardewModdingAPI.exe in your game folder to play with mods."); } else - this.PrintColor("SMAPI is removed! If you configured Steam to launch SMAPI, don't forget to clear your launch options.", ConsoleColor.DarkGreen); + this.PrintSuccess("SMAPI is removed! If you configured Steam to launch SMAPI, don't forget to clear your launch options."); } else { - if (action == ScriptAction.Install) - this.PrintColor("SMAPI is installed! Launch the game the same way as before to play with mods.", ConsoleColor.DarkGreen); - else - this.PrintColor("SMAPI is removed! Launch the game the same way as before to play without mods.", ConsoleColor.DarkGreen); + this.PrintSuccess(action == ScriptAction.Install + ? "SMAPI is installed! Launch the game the same way as before to play with mods." + : "SMAPI is removed! Launch the game the same way as before to play without mods." + ); } Console.ReadKey(); @@ -401,20 +408,6 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ - /// Test whether the current console supports color formatting. - private static bool GetConsoleSupportsColor() - { - try - { - Console.ForegroundColor = Console.ForegroundColor; - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } - /// Get the value of a key in the Windows registry. /// The full path of the registry key relative to HKLM. /// The name of the value. @@ -430,39 +423,19 @@ namespace StardewModdingApi.Installer /// Print a debug message. /// The text to print. - private void PrintDebug(string text) - { - this.PrintColor(text, ConsoleColor.DarkGray); - } + private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug); /// Print a warning message. /// The text to print. - private void PrintWarning(string text) - { - this.PrintColor(text, ConsoleColor.DarkYellow); - } + private void PrintWarning(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn); /// Print a warning message. /// The text to print. - private void PrintError(string text) - { - this.PrintColor(text, ConsoleColor.Red); - } + private void PrintError(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error); - /// Print a message to the console. - /// The message text. - /// The text foreground color. - private void PrintColor(string text, ConsoleColor color) - { - if (InteractiveInstaller.ConsoleSupportsColor) - { - Console.ForegroundColor = color; - Console.WriteLine(text); - Console.ResetColor(); - } - else - Console.WriteLine(text); - } + /// Print a success message. + /// The text to print. + private void PrintSuccess(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success); /// Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows. /// The current platform. @@ -671,10 +644,9 @@ namespace StardewModdingApi.Installer } /// Interactively move mods out of the appdata directory. - /// The current platform. /// The directory which should contain all mods. /// The installer directory containing packaged mods. - private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) + private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) { // get packaged mods to delete string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs index 5f8fe271..c04cf0e7 100644 --- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -95,7 +95,8 @@ namespace StardewModdingAPI.Internal.ConsoleWriting [ConsoleLogLevel.Info] = ConsoleColor.White, [ConsoleLogLevel.Warn] = ConsoleColor.Yellow, [ConsoleLogLevel.Error] = ConsoleColor.Red, - [ConsoleLogLevel.Alert] = ConsoleColor.Magenta + [ConsoleLogLevel.Alert] = ConsoleColor.Magenta, + [ConsoleLogLevel.Success] = ConsoleColor.DarkGreen }; case MonitorColorScheme.LightBackground: @@ -106,7 +107,8 @@ namespace StardewModdingAPI.Internal.ConsoleWriting [ConsoleLogLevel.Info] = ConsoleColor.Black, [ConsoleLogLevel.Warn] = ConsoleColor.DarkYellow, [ConsoleLogLevel.Error] = ConsoleColor.Red, - [ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta + [ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta, + [ConsoleLogLevel.Success] = ConsoleColor.DarkGreen }; default: diff --git a/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs b/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs index 85e69f51..54564111 100644 --- a/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs +++ b/src/SMAPI.Internal/ConsoleWriting/LogLevel.cs @@ -22,6 +22,9 @@ namespace StardewModdingAPI.Internal.ConsoleWriting Alert, /// A critical issue that generally signals an immediate end to the application. - Critical + Critical, + + /// A success message that generally signals a successful end to a task. + Success } } -- cgit From b83465060f3644c76444413be036f504434d4317 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:51:09 -0400 Subject: bump version for beta update --- src/SMAPI/Constants.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index e2d06228..3ad56896 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.8"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.9"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.9"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.10"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 75dfa884d99a778e694d2712ff70efac5e26725f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:53:19 -0400 Subject: fix documentation warnings --- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 1 + src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 1f37a1be..26fe7198 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -63,6 +63,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for managing console commands. /// an API for fetching metadata about loaded mods. /// An API for accessing private game code. + /// Provides multiplayer utilities. /// An API for reading translations stored in the mod's i18n folder. /// The content packs loaded for this mod. /// Create a transitional content pack. diff --git a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs index 5dd21b92..26b22315 100644 --- a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs +++ b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +#pragma warning disable 1591 // missing documentation namespace StardewModdingAPI.Framework.RewriteFacades { /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. -- cgit From f83a3bf7a44b41efd1c4b8d1426400b94668deae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 00:56:08 -0400 Subject: fix references to removed file --- build/prepare-install-package.targets | 2 -- src/SMAPI.Installer/InteractiveInstaller.cs | 1 - 2 files changed, 3 deletions(-) (limited to 'src') diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index d67d271f..6c431838 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -30,7 +30,6 @@ - @@ -44,7 +43,6 @@ - diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 5249355b..64dcc00e 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -87,7 +87,6 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.config.json"); yield return GetInstallPath("StardewModdingAPI.metadata.json"); - yield return GetInstallPath("StardewModdingAPI.Internal.dll"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); -- cgit From c05836040a7145c56f6af2dbc8fe4722efdf79c9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 01:13:10 -0400 Subject: fix some installer messages not using color scheme (#495) --- src/SMAPI.Installer/InteractiveInstaller.cs | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 64dcc00e..ba7a143c 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -211,7 +211,7 @@ namespace StardewModdingApi.Installer }; // show output - Console.WriteLine($"Your game folder: {installDir}."); + this.PrintInfo($"Your game folder: {installDir}."); /**** ** validate assumptions @@ -269,9 +269,9 @@ namespace StardewModdingApi.Installer action = ScriptAction.Uninstall; else { - Console.WriteLine("You can...."); - Console.WriteLine("[1] Install SMAPI."); - Console.WriteLine("[2] Uninstall SMAPI."); + this.PrintInfo("You can...."); + this.PrintInfo("[1] Install SMAPI."); + this.PrintInfo("[2] Uninstall SMAPI."); Console.WriteLine(); string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2"); @@ -424,6 +424,10 @@ namespace StardewModdingApi.Installer /// The text to print. private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug); + /// Print a debug message. + /// The text to print. + private void PrintInfo(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info); + /// Print a warning message. /// The text to print. private void PrintWarning(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn); @@ -529,11 +533,11 @@ namespace StardewModdingApi.Installer { while (true) { - Console.WriteLine(message); + this.PrintInfo(message); string input = Console.ReadLine()?.Trim().ToLowerInvariant(); if (!options.Contains(input)) { - Console.WriteLine("That's not a valid option."); + this.PrintInfo("That's not a valid option."); continue; } return input; @@ -584,9 +588,9 @@ namespace StardewModdingApi.Installer // let user choose path Console.WriteLine(); - Console.WriteLine("Found multiple copies of the game:"); + this.PrintInfo("Found multiple copies of the game:"); for (int i = 0; i < defaultPaths.Length; i++) - Console.WriteLine($"[{i + 1}] {defaultPaths[i].FullName}"); + this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}"); Console.WriteLine(); string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); @@ -596,15 +600,15 @@ namespace StardewModdingApi.Installer } // ask user - Console.WriteLine("Oops, couldn't find the game automatically."); + this.PrintInfo("Oops, couldn't find the game automatically."); while (true) { // get path from user - Console.WriteLine($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter."); + this.PrintInfo($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter."); string path = Console.ReadLine()?.Trim(); if (string.IsNullOrWhiteSpace(path)) { - Console.WriteLine(" You must specify a directory path to continue."); + this.PrintInfo(" You must specify a directory path to continue."); continue; } @@ -627,17 +631,17 @@ namespace StardewModdingApi.Installer // validate path if (!directory.Exists) { - Console.WriteLine(" That directory doesn't seem to exist."); + this.PrintInfo(" That directory doesn't seem to exist."); continue; } if (!directory.EnumerateFiles(executableFilename).Any()) { - Console.WriteLine(" That directory doesn't contain a Stardew Valley executable."); + this.PrintInfo(" That directory doesn't contain a Stardew Valley executable."); continue; } // looks OK - Console.WriteLine(" OK!"); + this.PrintInfo(" OK!"); return directory; } } -- cgit From 8000a5540a37804e3980ce165c7192d5badff585 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 19:05:45 -0400 Subject: fix various issues with content core rewrite (#488) --- src/SMAPI/Framework/ContentCoordinator.cs | 28 ++++++++++++++++++++++++---- src/SMAPI/Framework/SContentManager.cs | 31 ++++++++++++++++++++++++++++++- src/SMAPI/Framework/SGame.cs | 2 +- 3 files changed, 55 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 86ebc5c3..397a9d90 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework /// The loaded content managers (including the ). private readonly IList ContentManagers = new List(); + /// Whether the content coordinator has been disposed. + private bool IsDisposed; + /********* ** Accessors @@ -65,7 +68,7 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); this.ContentManagers.Add( - this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, isModFolder: false) + this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, isModFolder: false) ); this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection); } @@ -76,7 +79,9 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content (or null. for the default) public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null) { - return new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, isModFolder); + SContentManager manager = new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, isModFolder); + this.ContentManagers.Add(manager); + return manager; } /// Get the current content locale. @@ -162,8 +167,9 @@ namespace StardewModdingAPI.Framework /// Dispose held resources. public void Dispose() { - if (this.MainContentManager == null) - return; // already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); foreach (SContentManager contentManager in this.ContentManagers) @@ -171,5 +177,19 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Clear(); this.MainContentManager = null; } + + + /********* + ** Private methods + *********/ + /// A callback invoked when a content manager is disposed. + /// The content manager being disposed. + private void OnDisposing(SContentManager contentManager) + { + if (this.IsDisposed) + return; + + this.ContentManagers.Remove(contentManager); + } } } diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 8f008041..e9f46acb 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -43,12 +43,18 @@ namespace StardewModdingAPI.Framework /// The path prefix for assets in mod folders. private readonly string ModContentPrefix; + /// A callback to invoke when the content manager is being disposed. + private readonly Action OnDisposing; + /// Interceptors which provide the initial versions of matching assets. private IDictionary> Loaders => this.Coordinator.Loaders; /// Interceptors which edit matching assets after they're loaded. private IDictionary> Editors => this.Coordinator.Editors; + /// Whether the content coordinator has been disposed. + private bool IsDisposed; + /********* ** Accessors @@ -78,7 +84,8 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. /// Simplifies access to private code. /// Whether this content manager is wrapped around a mod folder. - public SContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, bool isModFolder) + /// A callback to invoke when the content manager is being disposed. + public SContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) : base(serviceProvider, rootDirectory, currentCulture) { // init @@ -88,6 +95,7 @@ namespace StardewModdingAPI.Framework this.Cache = new ContentCache(this, reflection); this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath, ContentSource.GameContent); + this.OnDisposing = onDisposing; // get asset data this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); @@ -291,6 +299,27 @@ namespace StardewModdingAPI.Framework return removeAssetNames; } + /// Dispose held resources. + /// Whether the content manager is being disposed (rather than finalized). + protected override void Dispose(bool isDisposing) + { + if (this.IsDisposed) + return; + this.IsDisposed = true; + + this.OnDisposing(this); + base.Dispose(isDisposing); + } + + /// + public override void Unload() + { + if (this.IsDisposed) + return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading + + base.Unload(); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 48a70688..8a4f987b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -209,7 +209,7 @@ namespace StardewModdingAPI.Framework if (this.NextContentManagerIsMain) { this.NextContentManagerIsMain = false; - return this.ContentCore.CreateContentManager("Game1.content", isModFolder: false, rootDirectory: rootDirectory); + return this.ContentCore.MainContentManager; } // any other content manager -- cgit From eda6ddbdc6c597916ba533c98ad51a97b42c0d1f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 May 2018 19:08:08 -0400 Subject: bump version for beta release --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 3ad56896..5a49ed6d 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.9"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.10"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.10"); -- cgit From 86a477d310c63edf7e9cc0547ef26e544ade1817 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 May 2018 00:16:10 -0400 Subject: fix content cache always missed when not playing in English --- src/SMAPI/Framework/SContentManager.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index e9f46acb..9353ee29 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -393,11 +393,16 @@ namespace StardewModdingAPI.Framework return this.Cache.ContainsKey(normalisedAssetName); // translated - if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) - return false; - return localisable - ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}") - : this.Cache.ContainsKey(normalisedAssetName); + string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) + { + return localisable + ? this.Cache.ContainsKey(localeKey) + : this.Cache.ContainsKey(normalisedAssetName); + } + + // not loaded yet + return false; } /**** -- cgit From a996aa12013366f565781067325a196411adb606 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 May 2018 00:19:19 -0400 Subject: bump version for beta release --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 5a49ed6d..a5dc63ce 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.10"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.11"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.10"); -- cgit From 69850cb1143110877ad0eea206a6537fe42c9063 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 May 2018 12:18:46 -0400 Subject: fix error when game looks up dialogue for a pet/horse with special characters in their name (#505) --- src/SMAPI/Framework/SContentManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 9353ee29..e97e655d 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -218,14 +218,16 @@ namespace StardewModdingAPI.Framework /// Assert that the given key has a valid format. /// The asset key to check. - /// The asset key is empty or contains invalid characters. + /// The asset key is empty or contains invalid characters. [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] public void AssertValidAssetKeyFormat(string key) { + // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid + // throwing other types like ArgumentException here. if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("The asset key or local path is empty."); + throw new SContentLoadException("The asset key or local path is empty."); if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new ArgumentException("The asset key or local path contains invalid characters."); + throw new SContentLoadException("The asset key or local path contains invalid characters."); } /// Convert an absolute file path into an appropriate asset name. -- cgit From 7b92d379793275905277cea7d0dec7de806e967b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 May 2018 12:49:28 -0400 Subject: fix unit test project --- build/common.targets | 2 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 44c2c071..fbb8c26c 100644 --- a/build/common.targets +++ b/build/common.targets @@ -30,7 +30,7 @@ - + diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 001b006c..f4d7b3e3 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -78,8 +78,7 @@ - - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. -- cgit From 59775e99c49790cf6632b0b6514f23c8209bece0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 May 2018 19:11:31 -0400 Subject: link smapi.io/install to install guide --- docs/release-notes.md | 1 + src/SMAPI.Web/Startup.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 03fec40d..cf531c57 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed detection of GOG Galaxy install path in rare cases. * Fixed install error on Linux/Mac in some cases. + * Fixed `smapi.io/install` not linking to a useful page. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 6c7ccecd..7f5b8e2b 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -153,9 +153,10 @@ namespace StardewModdingAPI.Web )); // shortcut redirects + redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1")); redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility")); redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); - redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1")); + redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI")); // redirect legacy canimod.com URLs var wikiRedirects = new Dictionary -- cgit From 28880395c73ce942a05c9a55ce8b379dc8bfee22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 May 2018 21:46:51 -0400 Subject: bump version for beta release --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a5dc63ce..257f6097 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.11"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.12"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.10"); -- cgit From b474c47a08813eef41db7f558f2efa33f6165d81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 15:42:36 -0400 Subject: update log parser for the internal 'critical' log level --- src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs | 17 +++++++++++------ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 6 ++++++ 2 files changed, 17 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs index 40d21bf8..759f15db 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs @@ -1,24 +1,29 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + namespace StardewModdingAPI.Web.Framework.LogParsing.Models { /// The log severity levels. public enum LogLevel { /// Tracing info intended for developers. - Trace, + Trace = ConsoleLogLevel.Trace, /// Troubleshooting info that may be relevant to the player. - Debug, + Debug = ConsoleLogLevel.Debug, /// Info relevant to the player. This should be used judiciously. - Info, + Info = ConsoleLogLevel.Info, /// An issue the player should be aware of. This should be used rarely. - Warn, + Warn = ConsoleLogLevel.Warn, /// A message indicating something went wrong. - Error, + Error = ConsoleLogLevel.Error, /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. - Alert + Alert = ConsoleLogLevel.Alert, + + /// A critical issue that generally signals an immediate end to the application. + Critical = ConsoleLogLevel.Critical } } diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 789274e2..25e874ac 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -200,6 +200,12 @@ table#metadata, table#mods { color: #f00; } +#log .critical { + background-color: #c00; + color: #fff; + font-weight: bold; +} + #log { border-spacing: 0; } -- cgit From ccd09eb9143bee60bfdc4182c3a7505fabddbe48 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 16:04:18 -0400 Subject: fix world_setseason command not running season change logic --- docs/release-notes.md | 1 + .../Framework/Commands/World/SetSeasonCommand.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index cf531c57..ff9fcdac 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * Fixed detection of GOG Galaxy install path in rare cases. * Fixed install error on Linux/Mac in some cases. * Fixed `smapi.io/install` not linking to a useful page. + * Fixed `world_setseason` command not running season-change logic. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index 897d052f..3d6d4b3d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -39,6 +39,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World // handle Game1.currentSeason = season; + Game1.setGraphicsForSeason(); monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); } } -- cgit From 5de6569bb1e63727e632d6459309b955e7ab7d5c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 17:34:40 -0400 Subject: minor cleanup --- build/common.targets | 1 - .../ConsoleCommandsMod.cs | 74 ---------------------- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 74 ++++++++++++++++++++++ .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 4 +- src/SMAPI.sln | 58 ----------------- src/SMAPI/Program.cs | 8 ++- 6 files changed, 83 insertions(+), 136 deletions(-) delete mode 100644 src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/ModEntry.cs (limited to 'src') diff --git a/build/common.targets b/build/common.targets index fbb8c26c..5065a6a6 100644 --- a/build/common.targets +++ b/build/common.targets @@ -101,7 +101,6 @@ - diff --git a/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs b/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs deleted file mode 100644 index 96658928..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI.Events; -using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; - -namespace StardewModdingAPI.Mods.ConsoleCommands -{ - /// The main entry point for the mod. - public class ConsoleCommandsMod : Mod - { - /********* - ** Properties - *********/ - /// The commands to handle. - private ITrainerCommand[] Commands; - - - /********* - ** Public methods - *********/ - /// The mod entry point, called after the mod is first loaded. - /// Provides simplified APIs for writing mods. - public override void Entry(IModHelper helper) - { - // register commands - this.Commands = this.ScanForCommands().ToArray(); - foreach (ITrainerCommand command in this.Commands) - helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); - - // hook events - GameEvents.UpdateTick += this.GameEvents_UpdateTick; - } - - - /********* - ** Private methods - *********/ - /// The method invoked when the game updates its state. - /// The event sender. - /// The event arguments. - private void GameEvents_UpdateTick(object sender, EventArgs e) - { - if (!Context.IsWorldReady) - return; - - foreach (ITrainerCommand command in this.Commands) - { - if (command.NeedsUpdate) - command.Update(this.Monitor); - } - } - - /// Handle a console command. - /// The command to invoke. - /// The command name specified by the user. - /// The command arguments. - private void HandleCommand(ITrainerCommand command, string commandName, string[] args) - { - ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor); - command.Handle(this.Monitor, commandName, argParser); - } - - /// Find all commands in the assembly. - private IEnumerable ScanForCommands() - { - return ( - from type in this.GetType().Assembly.GetTypes() - where !type.IsAbstract && typeof(ITrainerCommand).IsAssignableFrom(type) - select (ITrainerCommand)Activator.CreateInstance(type) - ); - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs new file mode 100644 index 00000000..7588043d --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Events; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; + +namespace StardewModdingAPI.Mods.ConsoleCommands +{ + /// The main entry point for the mod. + public class ModEntry : Mod + { + /********* + ** Properties + *********/ + /// The commands to handle. + private ITrainerCommand[] Commands; + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + // register commands + this.Commands = this.ScanForCommands().ToArray(); + foreach (ITrainerCommand command in this.Commands) + helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); + + // hook events + GameEvents.UpdateTick += this.GameEvents_UpdateTick; + } + + + /********* + ** Private methods + *********/ + /// The method invoked when the game updates its state. + /// The event sender. + /// The event arguments. + private void GameEvents_UpdateTick(object sender, EventArgs e) + { + if (!Context.IsWorldReady) + return; + + foreach (ITrainerCommand command in this.Commands) + { + if (command.NeedsUpdate) + command.Update(this.Monitor); + } + } + + /// Handle a console command. + /// The command to invoke. + /// The command name specified by the user. + /// The command arguments. + private void HandleCommand(ITrainerCommand command, string commandName, string[] args) + { + ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor); + command.Handle(this.Monitor, commandName, argParser); + } + + /// Find all commands in the assembly. + private IEnumerable ScanForCommands() + { + return ( + from type in this.GetType().Assembly.GetTypes() + where !type.IsAbstract && typeof(ITrainerCommand).IsAssignableFrom(type) + select (ITrainerCommand)Activator.CreateInstance(type) + ); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index d1f72c6c..357f34d7 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -38,7 +38,7 @@ ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - True + False @@ -77,7 +77,7 @@ - + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index c8760dcb..1e8ade24 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -66,98 +66,40 @@ Global SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Mixed Platforms.Build.0 = Debug|x86 {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.ActiveCfg = Debug|x86 {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Mixed Platforms.Build.0 = Release|x86 {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.ActiveCfg = Release|x86 {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Mixed Platforms.Build.0 = Debug|x86 {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.ActiveCfg = Debug|x86 {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Mixed Platforms.Build.0 = Release|x86 {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.ActiveCfg = Release|x86 {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Mixed Platforms.Build.0 = Debug|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.ActiveCfg = Debug|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.Build.0 = Debug|x86 {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.ActiveCfg = Debug|x86 {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.Build.0 = Release|x86 {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86 {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86 - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.ActiveCfg = Debug|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.Build.0 = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.ActiveCfg = Release|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.ActiveCfg = Debug|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.ActiveCfg = Release|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 1612ff11..e335b68e 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -97,6 +97,12 @@ namespace StardewModdingAPI new Regex(@"^DebugOutput: added CLOUD", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; + /// The mod IDs for which to not show missing update key warnings. + private readonly string[] AllowMissingUpdateKeys = + { + "SMAPI.ConsoleCommands" + }; + /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper = new JsonHelper(); @@ -783,7 +789,7 @@ namespace StardewModdingAPI : $" {metadata.DisplayName}...", LogLevel.Trace); // show warnings - if (metadata.HasManifest() && !metadata.HasUpdateKeys() && metadata.Manifest.UniqueID != "SMAPI.ConsoleCommands") + if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !this.AllowMissingUpdateKeys.Contains(metadata.Manifest.UniqueID)) this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); // validate status -- cgit From 92bfa6fa5b37e0f0c3f1967520d01a1947bf01bc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 18:16:40 -0400 Subject: add bundled save backup mod (#253) --- build/common.targets | 12 ++-- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs | 9 +++ src/SMAPI.Mods.SaveBackup/ModEntry.cs | 82 ++++++++++++++++++++++ .../Properties/AssemblyInfo.cs | 4 ++ .../StardewModdingAPI.Mods.SaveBackup.csproj | 58 +++++++++++++++ src/SMAPI.Mods.SaveBackup/manifest.json | 8 +++ src/SMAPI.sln | 6 ++ src/SMAPI/Program.cs | 3 +- 9 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs create mode 100644 src/SMAPI.Mods.SaveBackup/ModEntry.cs create mode 100644 src/SMAPI.Mods.SaveBackup/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj create mode 100644 src/SMAPI.Mods.SaveBackup/manifest.json (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 5065a6a6..b382ea54 100644 --- a/build/common.targets +++ b/build/common.targets @@ -30,7 +30,7 @@ - + @@ -95,7 +95,7 @@ - + @@ -106,10 +106,10 @@ - - - - + + + + diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 96d32090..5b3a5324 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.8", + "Version": "2.6.0-beta.12", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs b/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs new file mode 100644 index 00000000..c9dcb216 --- /dev/null +++ b/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI.Mods.SaveBackup.Framework +{ + /// The mod configuration. + internal class ModConfig + { + /// The number of backups to keep. + public int BackupsToKeep { get; set; } = 10; + } +} diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs new file mode 100644 index 00000000..e9e62752 --- /dev/null +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using StardewModdingAPI.Mods.SaveBackup.Framework; +using StardewValley; + +namespace StardewModdingAPI.Mods.SaveBackup +{ + /// The main entry point for the mod. + public class ModEntry : Mod + { + /********* + ** Properties + *********/ + /// The name of the save archive to create. + private readonly string FileName = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}.zip"; + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + try + { + // read config + ModConfig config = this.Helper.ReadConfig(); + + // init backup folder + DirectoryInfo folder = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "backups")); + folder.Create(); + + // back up saves + { + FileInfo file = new FileInfo(Path.Combine(folder.FullName, this.FileName)); + if (!file.Exists) + { + this.Monitor.Log($"Adding {file.Name}...", LogLevel.Trace); + ZipFile.CreateFromDirectory(Constants.SavesPath, file.FullName, CompressionLevel.Fastest, includeBaseDirectory: false); + } + } + + // prune old saves + foreach (FileInfo file in this.GetOldBackups(folder, config.BackupsToKeep)) + { + try + { + this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); + file.Delete(); + } + catch (Exception ex) + { + this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}"); + } + } + } + catch (Exception ex) + { + this.Monitor.Log($"Error backing up saves: {ex}"); + } + } + + + /********* + ** Private methods + *********/ + /// Get backups ordered by creation date. + /// The folder to search. + /// The number of backups to skip. + private IEnumerable GetOldBackups(DirectoryInfo folder, int skip) + { + return folder + .GetFiles() + .OrderByDescending(p => p.CreationTimeUtc) + .Skip(skip); + } + } +} diff --git a/src/SMAPI.Mods.SaveBackup/Properties/AssemblyInfo.cs b/src/SMAPI.Mods.SaveBackup/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..fc6b26fa --- /dev/null +++ b/src/SMAPI.Mods.SaveBackup/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("StardewModdingAPI.Mods.SaveBackup")] +[assembly: AssemblyDescription("")] diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj new file mode 100644 index 00000000..b322bf3a --- /dev/null +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -0,0 +1,58 @@ + + + + + Debug + x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4} + Library + Properties + StardewModdingAPI.Mods.SaveBackup + SaveBackup + v4.5 + 512 + + + true + full + false + $(SolutionDir)\..\bin\Debug\Mods\SaveBackup\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + $(SolutionDir)\..\bin\Release\Mods\SaveBackup\ + TRACE + prompt + 4 + + + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + PreserveNewest + + + + + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} + StardewModdingAPI + + + + + \ No newline at end of file diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json new file mode 100644 index 00000000..5246f65d --- /dev/null +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -0,0 +1,8 @@ +{ + "Name": "Save Backup", + "Author": "SMAPI", + "Version": "2.6.0-beta.12", + "Description": "Automatically backs up all your saves once per day into its folder.", + "UniqueID": "SMAPI.SaveBackup", + "EntryDll": "SaveBackup.dll" +} diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 1e8ade24..dec26694 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -58,6 +58,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyz EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\StardewModdingAPI.Mods.SaveBackup.csproj", "{E272EB5D-8C57-417E-8E60-C1079D3F53C4}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 @@ -102,6 +104,10 @@ Global {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.Build.0 = Debug|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.ActiveCfg = Release|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.Build.0 = Release|Any CPU + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|x86.ActiveCfg = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|x86.Build.0 = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|x86.ActiveCfg = Release|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e335b68e..f410abc1 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -100,7 +100,8 @@ namespace StardewModdingAPI /// The mod IDs for which to not show missing update key warnings. private readonly string[] AllowMissingUpdateKeys = { - "SMAPI.ConsoleCommands" + "SMAPI.ConsoleCommands", + "SMAPI.SaveBackup" }; /// Encapsulates SMAPI's JSON file parsing. -- cgit From b9036f212e7898f9cd13006024d63aec61d50ed6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 22:52:28 -0400 Subject: group mod warnings in console --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 7 ++++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 16 +++---- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 11 +++++ src/SMAPI/Framework/ModLoading/ModWarning.cs | 31 ++++++++++++++ src/SMAPI/Program.cs | 53 +++++++++++++++++++++--- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/ModWarning.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b531d7ec..b380f11d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. + * Improved how mod warnings are shown in the console. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed detection of GOG Galaxy install path in rare cases. * Fixed install error on Linux/Mac in some cases. diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b7972fe1..c0d6408d 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -24,6 +24,9 @@ namespace StardewModdingAPI.Framework /// The metadata resolution status. ModMetadataStatus Status { get; } + /// Indicates non-error issues with the mod. + ModWarning Warnings { get; } + /// The reason the metadata is invalid, if any. string Error { get; } @@ -52,6 +55,10 @@ namespace StardewModdingAPI.Framework /// Return the instance for chaining. IModMetadata SetStatus(ModMetadataStatus status, string error = null); + /// Set a warning flag for the mod. + /// The warning to set. + IModMetadata SetWarning(ModWarning warning); + /// Set the mod instance. /// The mod instance to set. IModMetadata SetMod(IMod mod); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 6c0e1c14..2fb2aba7 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,33 +285,27 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); if (!assumeCompatible) throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found broken code ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); + mod.SetWarning(ModWarning.ChangesSaveSerialiser); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); + mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", -#if SMAPI_FOR_WINDOWS - this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug -#else - LogLevel.Warn -#endif - ); + mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.None: diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index d3a33e7a..e4ec7e3b 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -25,6 +25,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } + /// Indicates non-error issues with the mod. + public ModWarning Warnings { get; private set; } + /// The reason the metadata is invalid, if any. public string Error { get; private set; } @@ -71,6 +74,14 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } + /// Set a warning flag for the mod. + /// The warning to set. + public IModMetadata SetWarning(ModWarning warning) + { + this.Warnings |= warning; + return this; + } + /// Set the mod instance. /// The mod instance to set. public IModMetadata SetMod(IMod mod) diff --git a/src/SMAPI/Framework/ModLoading/ModWarning.cs b/src/SMAPI/Framework/ModLoading/ModWarning.cs new file mode 100644 index 00000000..0e4b2570 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModWarning.cs @@ -0,0 +1,31 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates a detected non-error mod issue. + [Flags] + internal enum ModWarning + { + /// No issues detected. + None = 0, + + /// SMAPI detected incompatible code in the mod, but was configured to load it anyway. + BrokenCodeLoaded = 1, + + /// The mod affects the save serializer in a way that may make saves unloadable without the mod. + ChangesSaveSerialiser = 2, + + /// The mod patches the game in a way that may impact stability. + PatchesGame = 4, + + /// The mod uses the dynamic keyword which won't work on Linux/Mac. + UsesDynamic = 8, + + /// The mod references which may impact stability. + UsesUnvalidatedUpdateTick = 16, + + /// The mod has no update keys set. + NoUpdateKeys = 32 + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f410abc1..e50c4ec9 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -742,7 +742,7 @@ namespace StardewModdingAPI // show warning for missing update key if (metadata.HasManifest() && !metadata.HasUpdateKeys()) - this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status if (metadata.Status == ModMetadataStatus.Failed) @@ -791,7 +791,7 @@ namespace StardewModdingAPI // show warnings if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !this.AllowMissingUpdateKeys.Contains(metadata.Manifest.UniqueID)) - this.Monitor.Log($" {metadata.DisplayName} has no {nameof(IManifest.UpdateKeys)} in its manifest. You may not see update alerts for this mod.", LogLevel.Warn); + metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status if (metadata.Status == ModMetadataStatus.Failed) @@ -831,6 +831,10 @@ namespace StardewModdingAPI // initialise mod try { + // get mod instance + if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + continue; + // get content packs if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) contentPacks = new IContentPack[0]; @@ -858,10 +862,6 @@ namespace StardewModdingAPI modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } - // get mod instance - if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) - continue; - // init mod mod.ModManifest = manifest; mod.Helper = modHelper; @@ -931,6 +931,28 @@ namespace StardewModdingAPI this.Monitor.Newline(); } + // log warnings + { + IModMetadata[] modsWithWarnings = this.ModRegistry.GetAll().Where(p => p.Warnings != ModWarning.None).ToArray(); + if (modsWithWarnings.Any()) + { + this.Monitor.Log($"Found issues with {modsWithWarnings.Length} mods:", LogLevel.Warn); + foreach (IModMetadata metadata in modsWithWarnings) + { + string[] warnings = this.GetWarningText(metadata.Warnings).ToArray(); + if (warnings.Length == 1) + this.Monitor.Log($" {metadata.DisplayName}: {warnings[0]}", LogLevel.Warn); + else + { + this.Monitor.Log($" {metadata.DisplayName}:", LogLevel.Warn); + foreach (string warning in warnings) + this.Monitor.Log(" - " + warning, LogLevel.Warn); + } + } + this.Monitor.Newline(); + } + } + // initialise translations this.ReloadTranslations(loadedMods); @@ -1020,6 +1042,25 @@ namespace StardewModdingAPI this.ModRegistry.AreAllModsInitialised = true; } + /// Get the warning text for a mod warning bit mask. + /// The mod warning bit mask. + private IEnumerable GetWarningText(ModWarning mask) + { + if (mask.HasFlag(ModWarning.BrokenCodeLoaded)) + yield return "has broken code, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly."; + if (mask.HasFlag(ModWarning.ChangesSaveSerialiser)) + yield return "accesses the save serialiser and may break your saves."; + if (mask.HasFlag(ModWarning.PatchesGame)) + yield return "patches the game. This may cause errors or bugs in-game. If you have issues, try removing this mod first."; + if (mask.HasFlag(ModWarning.UsesUnvalidatedUpdateTick)) + yield return "bypasses normal SMAPI event protections. This may cause errors or save corruption. If you have issues, try removing this mod first."; + if (mask.HasFlag(ModWarning.UsesDynamic)) + yield return "uses the 'dynamic' keyword. This won't work on Linux/Mac."; + if (mask.HasFlag(ModWarning.NoUpdateKeys)) + yield return "has no update keys in its manifest. SMAPI won't show update alerts for this mod."; + + } + /// Load a mod's entry class. /// The mod assembly. /// A callback invoked when loading fails. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 01d5aaf1..19c1e6fe 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -113,6 +113,7 @@ + -- cgit From 5d32d8cba7a83e06c0543a079ba2ea25873ced91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 23:02:08 -0400 Subject: change 'outdated' to 'not compatible' due to modder confusion --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e50c4ec9..3125bead 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -814,7 +814,7 @@ namespace StardewModdingAPI { string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - TrackSkip(metadata, $"it's outdated. Please check for a new version at {string.Join(" or ", updateUrls)}."); + TrackSkip(metadata, $"it's not compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); continue; } catch (SAssemblyLoadFailedException ex) -- cgit From c0c41ac851ad6b49ea77ace1b0e6455f3cff4ada Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 May 2018 23:37:17 -0400 Subject: bump version for beta release --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 257f6097..5347deff 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.12"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.13"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.10"); -- cgit From 445b29b1958b8f4d425ee62cc6bdec4ff58da5d4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 13 May 2018 22:34:10 -0400 Subject: update compatibility list --- src/SMAPI/StardewModdingAPI.metadata.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 9f229b04..33568692 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -394,8 +394,8 @@ "Content Patcher": { "ID": "Pathoschild.ContentPatcher", - "Default | UpdateKey": "Nexus:1915", - "~1.3.1 | Status": "AssumeBroken" // broke in SDV 1.3 (in-game errors) + "Default | UpdateKey": "Nexus:1915", + "~1.4-beta.2 | Status": "AssumeBroken" // broke in SDV 1.3 (in-game errors) }, "Cooking Skill": { @@ -1655,8 +1655,8 @@ "TractorMod": { "ID": "Pathoschild.TractorMod", "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Default | UpdateKey": "Nexus:1401", - "~4.4.1 | Status": "AssumeBroken" // broke in SDV 1.3 + "Default | UpdateKey": "Nexus:1401", + "~4.5-beta | Status": "AssumeBroken" // broke in SDV 1.3 }, "TrainerMod": { -- cgit From 47cfd6cd3d9a5f57be48a638bc8feca32006f7af Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 13 May 2018 23:03:31 -0400 Subject: tweak not-compatible messages --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index cec37fca..b5339183 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModLoading case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's outdated"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List updateUrls = new List(); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3125bead..6aff6dc6 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -814,7 +814,7 @@ namespace StardewModdingAPI { string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - TrackSkip(metadata, $"it's not compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); + TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); continue; } catch (SAssemblyLoadFailedException ex) -- cgit From df1d856cb00e8dbc6a6ef7a03990d943a7f3f4de Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 00:48:13 -0400 Subject: fix typo in command help --- .../Framework/Commands/Player/SetStyleCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs index b59be2e5..31f4107d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -1,4 +1,4 @@ -using StardewValley; +using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player *********/ /// Construct an instance. public SetStyleCommand() - : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { } + : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changestyle .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { } /// Handle the command. /// Writes messages to the console and log file. -- cgit From 639302aa65b174304789e9c4862d4cce2d74150d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 00:48:35 -0400 Subject: fix Save Backup including unneeded DLLs in mod folder --- src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index b322bf3a..89e92a8a 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -51,6 +51,7 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + False -- cgit From 57aa4016abaa1385fbc954c62e3f76d9496c6a42 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 01:08:47 -0400 Subject: fix crossplatform error due to Netcode references not being rewritten (#514) --- src/SMAPI/Constants.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 5347deff..bea4bb20 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -117,6 +117,7 @@ namespace StardewModdingAPI case Platform.Mac: removeAssemblyReferences = new[] { + "Netcode", "Stardew Valley", "Microsoft.Xna.Framework", "Microsoft.Xna.Framework.Game", @@ -124,6 +125,7 @@ namespace StardewModdingAPI }; targetAssemblies = new[] { + typeof(Netcode.NetBool).Assembly, typeof(StardewValley.Game1).Assembly, typeof(Microsoft.Xna.Framework.Vector2).Assembly }; @@ -132,11 +134,13 @@ namespace StardewModdingAPI case Platform.Windows: removeAssemblyReferences = new[] { + "Netcode", "StardewValley", "MonoGame.Framework" }; targetAssemblies = new[] { + typeof(Netcode.NetBool).Assembly, typeof(StardewValley.Game1).Assembly, typeof(Microsoft.Xna.Framework.Vector2).Assembly, typeof(Microsoft.Xna.Framework.Game).Assembly, -- cgit From 75af88cf0d09b0ebc9275968c9e5abd3ea179ea6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 01:45:02 -0400 Subject: fix installer deleting Omegasis' SaveBackup mod (#513) --- src/SMAPI.Installer/InteractiveInstaller.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index ba7a143c..4d9bc984 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -351,11 +351,25 @@ namespace StardewModdingApi.Installer } // add or replace bundled mods - Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods")); + modsDir.Create(); DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods")); if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) { this.PrintDebug("Adding bundled mods..."); + + // special case: rename Omegasis' SaveBackup mod + { + DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup")); + DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup (Omegasis)")); + FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json")); + if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1)) + { + this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}..."); + this.Move(oldFolder, newFolder.FullName); + } + } + + // add bundled mods foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) { this.PrintDebug($" adding {sourceDir.Name}..."); -- cgit From 28986e76b81fe4505574e4fada3d86ffa457f085 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 02:01:21 -0400 Subject: tweak renamed folder per discussion with Omegasis (#513) --- src/SMAPI.Installer/InteractiveInstaller.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 4d9bc984..ace560e5 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -360,7 +360,7 @@ namespace StardewModdingApi.Installer // special case: rename Omegasis' SaveBackup mod { DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup")); - DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup (Omegasis)")); + DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "AdvancedSaveBackup")); FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json")); if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1)) { -- cgit From 9d3e37317893126a9e0598f8f850abd23cad36c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 16:14:41 -0400 Subject: update for Stardew Valley 1.3.11 (#521) --- .../Framework/StateTracking/FieldWatchers/WatcherFactory.cs | 9 +++++++++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index a4982faa..4f1ac9f4 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -36,6 +36,15 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new ObservableCollectionWatcher(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net field instance type. + /// The net collection. + public static NetValueWatcher ForNetValue(NetFieldBase field) where TSelf : NetFieldBase + { + return new NetValueWatcher(field); + } + /// Get a watcher for a net collection. /// The value type. /// The net collection. diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index dea2e30d..3814e534 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -59,12 +59,12 @@ namespace StardewModdingAPI.Framework.StateTracking this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { - [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForEquatable(() => player.combatLevel), - [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForEquatable(() => player.farmingLevel), - [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForEquatable(() => player.fishingLevel), - [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForEquatable(() => player.foragingLevel), - [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForEquatable(() => player.luckLevel), - [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForEquatable(() => player.miningLevel) + [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), + [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), + [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), + [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), + [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), + [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) }; // track watchers for convenience -- cgit From 5c1068c30ca0a030f924c05d7bae603e887e5252 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 16:16:07 -0400 Subject: bump versions for release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 5b3a5324..50d25697 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.12", + "Version": "2.6.0-beta.14", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 5246f65d..86f85e26 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.12", + "Version": "2.6.0-beta.14", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bea4bb20..9232d648 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.13"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.14"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.10"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.11"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From fae2bc9de57e9e82b06823046cacb615ea878a5d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 16:25:29 -0400 Subject: fix assembly mapping error on Linux/Mac (#514) --- src/SMAPI/Constants.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 9232d648..cf958ae0 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -125,8 +125,7 @@ namespace StardewModdingAPI }; targetAssemblies = new[] { - typeof(Netcode.NetBool).Assembly, - typeof(StardewValley.Game1).Assembly, + typeof(StardewValley.Game1).Assembly, // note: includes Netcode types on Linux/Mac typeof(Microsoft.Xna.Framework.Vector2).Assembly }; break; -- cgit From 13e55fb2aa0ff9fea401262f82d8ecbcf835f21c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 May 2018 19:26:28 -0400 Subject: fix typo in config comment --- src/SMAPI/StardewModdingAPI.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index f37c6fc1..6725dbbd 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -46,7 +46,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * The console color theme to use. The possible values are: - * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Mac. + * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. * - LightBackground: use darker text colors that look better on a white or light background. * - DarkBackground: use lighter text colors that look better on a black or dark background. */ -- cgit From 4eebd813f239267d659f3cbf4fa6cf5d47d99c26 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 May 2018 19:26:53 -0400 Subject: add beta for-developers download to smapi.io --- src/SMAPI.Internal/SemanticVersionImpl.cs | 2 +- src/SMAPI.Web/Controllers/IndexController.cs | 144 ++++++++++++++++----------- src/SMAPI.Web/Views/Index/Index.cshtml | 4 + 3 files changed, 90 insertions(+), 60 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Internal/SemanticVersionImpl.cs b/src/SMAPI.Internal/SemanticVersionImpl.cs index 6da16336..7ae34f07 100644 --- a/src/SMAPI.Internal/SemanticVersionImpl.cs +++ b/src/SMAPI.Internal/SemanticVersionImpl.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Internal { /// A low-level implementation of a semantic version with an optional release tag. /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). - internal class SemanticVersionImpl + internal class SemanticVersionImpl : IComparable { /********* ** Accessors diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 92b4f2c0..08b7363a 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -46,24 +48,23 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] public async Task Index() { - // fetch SMAPI releases - IndexVersionModel stableVersion = await this.Cache.GetOrCreateAsync("stable-version", async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); - return new IndexVersionModel(release.Name, release.Body, this.GetMainDownloadUrl(release), this.GetDevDownloadUrl(release)); - }); - IndexVersionModel betaVersion = await this.Cache.GetOrCreateAsync("beta-version", async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); - return release.IsPrerelease - ? this.GetBetaDownload(release) - : null; - }); + // choose versions + ReleaseVersion[] versions = await this.GetReleaseVersionsAsync(); + ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsBeta && !version.IsForDevs); + ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => !version.IsBeta && version.IsForDevs); + ReleaseVersion betaVersion = versions.LastOrDefault(version => version.IsBeta && !version.IsForDevs); + ReleaseVersion betaVersionForDevs = versions.LastOrDefault(version => version.IsBeta && version.IsForDevs); // render view - var model = new IndexModel(stableVersion, betaVersion); + IndexVersionModel stableVersionModel = stableVersion != null + ? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl) + : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong) + IndexVersionModel betaVersionModel = betaVersion != null + ? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl) + : null; + + // render view + var model = new IndexModel(stableVersionModel, betaVersionModel); return this.View(model); } @@ -71,62 +72,87 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Private methods *********/ - /// Get the main download URL for a SMAPI release. - /// The SMAPI release. - private string GetMainDownloadUrl(GitRelease release) + /// Get a sorted, parsed list of SMAPI downloads for the latest releases. + private async Task GetReleaseVersionsAsync() { - // get main download URL - foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + return await this.Cache.GetOrCreateAsync("available-versions", async entry => { - if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer.zip")) - return asset.DownloadUrl; - } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - // fallback just in case - return "https://github.com/pathoschild/SMAPI/releases"; + // get releases + GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); + GitRelease betaRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); + if (stableRelease.Tag == betaRelease.Tag) + betaRelease = null; + + // get versions + ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray(); + ReleaseVersion[] betaVersions = this.ParseReleaseVersions(betaRelease).ToArray(); + return stableVersions + .Concat(betaVersions) + .OrderBy(p => p.Version) + .ToArray(); + }); } - /// Get the for-developers download URL for a SMAPI release. - /// The SMAPI release. - private string GetDevDownloadUrl(GitRelease release) + /// Get a parsed list of SMAPI downloads for a release. + /// The GitHub release. + private IEnumerable ParseReleaseVersions(GitRelease release) { - // get dev download URL - foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + if (release?.Assets == null) + yield break; + + foreach (GitAsset asset in release.Assets) { - if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer-for-developers.zip")) - return asset.DownloadUrl; - } + Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); + if (!match.Success || !SemanticVersionImpl.TryParse(match.Groups["version"].Value, out SemanticVersionImpl version)) + continue; + bool isBeta = version.Tag != null; + bool isForDevs = match.Groups["forDevs"].Success; - // fallback just in case - return "https://github.com/pathoschild/SMAPI/releases"; + yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); + } } - /// Get the latest beta download for a SMAPI release. - /// The SMAPI release. - private IndexVersionModel GetBetaDownload(GitRelease release) + /// A parsed release download. + private class ReleaseVersion { - // get download with the latest version - SemanticVersionImpl latestVersion = null; - string latestUrl = null; - foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + /********* + ** Accessors + *********/ + /// The underlying GitHub release. + public GitRelease Release { get; } + + /// The underlying download asset. + public GitAsset Asset { get; } + + /// The SMAPI version. + public SemanticVersionImpl Version { get; } + + /// Whether this is a beta download. + public bool IsBeta { get; } + + /// Whether this is a 'for developers' download. + public bool IsForDevs { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying GitHub release. + /// The underlying download asset. + /// The SMAPI version. + /// Whether this is a beta download. + /// Whether this is a 'for developers' download. + public ReleaseVersion(GitRelease release, GitAsset asset, SemanticVersionImpl version, bool isBeta, bool isForDevs) { - // parse version - Match versionMatch = Regex.Match(asset.FileName, @"SMAPI-([\d\.]+(?:-.+)?)-installer.zip"); - if (!versionMatch.Success || !SemanticVersionImpl.TryParse(versionMatch.Groups[1].Value, out SemanticVersionImpl version)) - continue; - - // save latest version - if (latestVersion == null || latestVersion.CompareTo(version) < 0) - { - latestVersion = version; - latestUrl = asset.DownloadUrl; - } + this.Release = release; + this.Asset = asset; + this.Version = version; + this.IsBeta = isBeta; + this.IsForDevs = isForDevs; } - - // return if prerelease - return latestVersion?.Tag != null - ? new IndexVersionModel(latestVersion.ToString(), release.Body, latestUrl, null) - : null; } } } diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 347eebc7..91cdc793 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -86,6 +86,10 @@ else

For mod creators

-- cgit From 8e2d8b97f047e6d79a3e424ba0dc611388cb65cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 May 2018 19:27:16 -0400 Subject: fix mod update checks failing if a mod only has prerelease versions on GitHub --- docs/release-notes.md | 1 + .../Framework/ModRepositories/GitHubRepository.cs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b380f11d..118cc441 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * Fixed install error on Linux/Mac in some cases. * Fixed `smapi.io/install` not linking to a useful page. * Fixed `world_setseason` command not running season-change logic. + * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index b12b24e2..f4abd379 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -38,21 +38,25 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories // fetch info try { - // get latest release + // get latest release (whether preview or stable) GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); - GitRelease preview = null; if (latest == null) return new ModInfoModel("Found no mod with this ID."); - // get latest stable release (if not latest) + // split stable/prerelease if applicable + GitRelease preview = null; if (latest.IsPrerelease) { - preview = latest; - latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); + GitRelease result = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); + if (result != null) + { + preview = latest; + latest = result; + } } // return data - return new ModInfoModel(name: id, version: this.NormaliseVersion(latest?.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases"); + return new ModInfoModel(name: id, version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases"); } catch (Exception ex) { -- cgit From 3e36af75d59dbd539cfb2c304281f64f1753fc9f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 19 May 2018 00:05:53 -0400 Subject: fix SMAPI rewriting Windows mods unnecessarily (#523) --- src/SMAPI/Constants.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index cf958ae0..fc295f28 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -133,7 +133,6 @@ namespace StardewModdingAPI case Platform.Windows: removeAssemblyReferences = new[] { - "Netcode", "StardewValley", "MonoGame.Framework" }; -- cgit From a4f644e3c7d4e86a5164099bfc9050b4b164ae0e Mon Sep 17 00:00:00 2001 From: kurumushi Date: Sun, 20 May 2018 16:49:43 +0900 Subject: Change konsole launch options for newer ncurses This makes konsole tell mono that it is xterm, allowing it to avoid mono/mono#6752 and providing a workaround for #489. --- src/SMAPI.Installer/unix-launcher.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index 6e796461..f16cc317 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -78,7 +78,7 @@ else elif $COMMAND gnome-terminal 2>/dev/null; then gnome-terminal -e "$LAUNCHER" elif $COMMAND konsole 2>/dev/null; then - konsole -e "$LAUNCHER" + konsole -p Environment=TERM=xterm -e "$LAUNCHER" elif $COMMAND terminal 2>/dev/null; then terminal -e "$LAUNCHER" else -- cgit From bd04d46dd1d66b30d4f21575bbbd2e541eabcef3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 22 May 2018 22:53:44 -0400 Subject: refactor content API to fix load errors with decentralised cache (#524) --- docs/release-notes.md | 2 + src/SMAPI/Framework/ContentCoordinator.cs | 123 +++- .../ContentManagers/BaseContentManager.cs | 268 +++++++++ .../ContentManagers/GameContentManager.cs | 252 ++++++++ .../Framework/ContentManagers/IContentManager.cs | 81 +++ .../Framework/ContentManagers/ModContentManager.cs | 207 +++++++ src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 66 ++- src/SMAPI/Framework/SContentManager.cs | 653 --------------------- src/SMAPI/Framework/SGame.cs | 4 +- src/SMAPI/Framework/Utilities/PathUtilities.cs | 7 +- src/SMAPI/Program.cs | 9 +- src/SMAPI/StardewModdingAPI.csproj | 5 +- 12 files changed, 968 insertions(+), 709 deletions(-) create mode 100644 src/SMAPI/Framework/ContentManagers/BaseContentManager.cs create mode 100644 src/SMAPI/Framework/ContentManagers/GameContentManager.cs create mode 100644 src/SMAPI/Framework/ContentManagers/IContentManager.cs create mode 100644 src/SMAPI/Framework/ContentManagers/ModContentManager.cs delete mode 100644 src/SMAPI/Framework/SContentManager.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 118cc441..b053789d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -32,6 +32,8 @@ * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. + * Fixed mods able to intercept other mods' assets via the internal asset keys. + * Fixed mods able to indirectly change other mods' data through shared content caches. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten (see above). diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 397a9d90..c2614001 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -5,10 +5,14 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; using StardewValley; +using xTile; namespace StardewModdingAPI.Framework { @@ -18,6 +22,9 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ + /// An asset key prefix for assets from SMAPI mod folders. + private readonly string ManagedPrefix = "SMAPI"; + /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; @@ -28,7 +35,7 @@ namespace StardewModdingAPI.Framework private readonly Reflector Reflection; /// The loaded content managers (including the ). - private readonly IList ContentManagers = new List(); + private readonly IList ContentManagers = new List(); /// Whether the content coordinator has been disposed. private bool IsDisposed; @@ -38,7 +45,7 @@ namespace StardewModdingAPI.Framework ** Accessors *********/ /// The primary content manager used for most assets. - public SContentManager MainContentManager { get; private set; } + public GameContentManager MainContentManager { get; private set; } /// The current language as a constant. public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; @@ -68,28 +75,110 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); this.ContentManagers.Add( - this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, isModFolder: false) + this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing) ); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormaliseAssetName, reflection); + } + + /// Get a new content manager which handles reading files from the game content folder with support for interception. + /// 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.ContentManagers.Add(manager); + return manager; } - /// Get a new content manager which defers loading to the content core. + /// Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files. /// A name for the mod manager. Not guaranteed to be unique. - /// Whether this content manager is wrapped around a mod folder. - /// The root directory to search for content (or null. for the default) - public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null) + /// The root directory to search for content (or null for the default). + public ModContentManager CreateModContentManager(string name, string rootDirectory) { - SContentManager manager = new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, isModFolder); + ModContentManager manager = new ModContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing); this.ContentManagers.Add(manager); return manager; } /// Get the current content locale. - public string GetLocale() => this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + public string GetLocale() + { + return this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + } + + /// Get whether this asset is mapped to a mod folder. + /// The asset key. + public bool IsManagedAssetKey(string key) + { + return key.StartsWith(this.ManagedPrefix); + } + + /// Parse a managed SMAPI asset key which maps to a mod folder. + /// The asset key. + /// The unique name for the content manager which should load this asset. + /// The relative path within the mod folder. + /// Returns whether the asset was parsed successfully. + public bool TryParseManagedAssetKey(string key, out string contentManagerID, out string relativePath) + { + contentManagerID = null; + relativePath = null; + + // not a managed asset + if (!key.StartsWith(this.ManagedPrefix)) + return false; - /// Convert an absolute file path into a appropriate asset name. - /// The absolute path to the file. - public string GetAssetNameFromFilePath(string absolutePath) => this.MainContentManager.GetAssetNameFromFilePath(absolutePath, ContentSource.GameContent); + // parse + string[] parts = PathUtilities.GetSegments(key, 3); + if (parts.Length != 3) // managed key prefix, mod id, relative path + return false; + contentManagerID = Path.Combine(parts[0], parts[1]); + relativePath = parts[2]; + return true; + } + + /// Get the managed asset key prefix for a mod. + /// The mod's unique ID. + public string GetManagedAssetPrefix(string modID) + { + return Path.Combine(this.ManagedPrefix, modID.ToLower()); + } + + /// Get a copy of an asset from a mod folder. + /// The asset type. + /// The internal asset key. + /// The unique name for the content manager which should load this asset. + /// The internal SMAPI asset key. + /// The language code for which to load content. + public T LoadAndCloneManagedAsset(string internalKey, string contentManagerID, string relativePath, LocalizedContentManager.LanguageCode language) + { + // get content manager + IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.Name == contentManagerID); + if (contentManager == null) + throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); + + // get cloned asset + T data = contentManager.Load(internalKey, language); + switch (data as object) + { + case Texture2D source: + { + int[] pixels = new int[source.Width * source.Height]; + source.GetData(pixels); + + Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); + clone.SetData(pixels); + return (T)(object)clone; + } + + case Dictionary source: + return (T)(object)new Dictionary(source); + + case Dictionary source: + return (T)(object)new Dictionary(source); + + default: + return data; + } + } /// Purge assets from the cache that match one of the interceptors. /// The asset editors for which to purge matching assets. @@ -129,7 +218,7 @@ namespace StardewModdingAPI.Framework string locale = this.GetLocale(); return this.InvalidateCache((assetName, type) => { - IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.NormaliseAssetName); + IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormaliseAssetName); return predicate(info); }); } @@ -142,7 +231,7 @@ namespace StardewModdingAPI.Framework { // invalidate cache HashSet removedAssetNames = new HashSet(); - foreach (SContentManager contentManager in this.ContentManagers) + foreach (IContentManager contentManager in this.ContentManagers) { foreach (string name in contentManager.InvalidateCache(predicate, dispose)) removedAssetNames.Add(name); @@ -172,7 +261,7 @@ namespace StardewModdingAPI.Framework this.IsDisposed = true; this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); - foreach (SContentManager contentManager in this.ContentManagers) + foreach (IContentManager contentManager in this.ContentManagers) contentManager.Dispose(); this.ContentManagers.Clear(); this.MainContentManager = null; @@ -184,7 +273,7 @@ namespace StardewModdingAPI.Framework *********/ /// A callback invoked when a content manager is disposed. /// The content manager being disposed. - private void OnDisposing(SContentManager contentManager) + private void OnDisposing(IContentManager contentManager) { if (this.IsDisposed) return; diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs new file mode 100644 index 00000000..ff0e2de4 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + internal abstract class BaseContentManager : LocalizedContentManager, IContentManager + { + /********* + ** Properties + *********/ + /// The central coordinator which manages content managers. + protected readonly ContentCoordinator Coordinator; + + /// The underlying asset cache. + protected readonly ContentCache Cache; + + /// Encapsulates monitoring and logging. + protected readonly IMonitor Monitor; + + /// Whether the content coordinator has been disposed. + private bool IsDisposed; + + /// The language enum values indexed by locale code. + private readonly IDictionary LanguageCodes; + + /// A callback to invoke when the content manager is being disposed. + private readonly Action OnDisposing; + + + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + public string Name { get; } + + /// The current language as a constant. + public LanguageCode Language => this.GetCurrentLanguage(); + + /// The absolute path to the . + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + /// Whether this content manager is for a mod folder. + public bool IsModContentManager { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + /// Whether this content manager is for a mod folder. + protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) + : base(serviceProvider, rootDirectory, currentCulture) + { + // init + this.Name = name; + this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); + this.Cache = new ContentCache(this, reflection); + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.OnDisposing = onDisposing; + this.IsModContentManager = isModFolder; + + // get asset data + this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + } + + /// 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.Load(assetName, LocalizedContentManager.CurrentLanguageCode); + } + + /// Load the base asset without localisation. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T LoadBase(string assetName) + { + return this.Load(assetName, LanguageCode.en); + } + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + public void Inject(string assetName, T value) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + this.Cache[assetName] = value; + + } + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + return this.Cache.NormalisePathSeparators(path); + } + + /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public string AssertAndNormaliseAssetName(string assetName) + { + // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid + // throwing other types like ArgumentException here. + if (string.IsNullOrWhiteSpace(assetName)) + throw new SContentLoadException("The asset key or local path is empty."); + if (assetName.Intersect(Path.GetInvalidPathChars()).Any()) + throw new SContentLoadException("The asset key or local path contains invalid characters."); + + return this.Cache.NormaliseKey(assetName); + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetLocale(this.GetCurrentLanguage()); + } + + /// The locale for a language. + /// The language. + public string GetLocale(LanguageCode language) + { + return this.LanguageCodeString(language); + } + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public bool IsLoaded(string assetName) + { + assetName = this.Cache.NormaliseKey(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } + + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + return this.Cache.Keys + .Select(this.GetAssetName) + .Distinct(); + } + + /**** + ** Cache invalidation + ****/ + /// 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 the number of invalidated assets. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => + { + this.ParseCacheKey(key, out string assetName, out _); + + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) + { + removeAssetNames.Add(assetName); + return true; + } + return false; + }); + + return removeAssetNames; + } + + /// Dispose held resources. + /// Whether the content manager is being disposed (rather than finalized). + protected override void Dispose(bool isDisposing) + { + if (this.IsDisposed) + return; + this.IsDisposed = true; + + this.OnDisposing(this); + base.Dispose(isDisposing); + } + + /// + public override void Unload() + { + if (this.IsDisposed) + return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading + + base.Unload(); + } + + + /********* + ** Private methods + *********/ + /// Get the locale codes (like ja-JP) used in asset keys. + private IDictionary GetKeyLocales() + { + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset name. + /// The asset locale code (or null if not localised). + protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.LanguageCodes.ContainsKey(suffix)) + { + assetName = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetName = cacheKey; + localeCode = null; + } + + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName); + } +} diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs new file mode 100644 index 00000000..cfedb5af --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from the game content folder with support for interception. + internal class GameContentManager : BaseContentManager + { + /********* + ** Properties + *********/ + /// 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(); + + /// Interceptors which provide the initial versions of matching assets. + private IDictionary> Loaders => this.Coordinator.Loaders; + + /// Interceptors which edit matching assets after they're loaded. + private IDictionary> Editors => this.Coordinator.Editors; + + /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalisableLookup; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false) + { + this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + } + + /// 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 language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + + // get from cache + if (this.IsLoaded(assetName)) + return base.Load(assetName, language); + + // get managed asset + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); + this.Inject(assetName, managedAsset); + return managedAsset; + } + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName, language); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + string locale = this.GetLocale(language); + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName); + IAssetData asset = + this.ApplyLoader(info) + ?? new AssetDataForObject(info, base.Load(assetName, language), this.AssertAndNormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Inject(assetName, data); + return data; + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + return this.Coordinator.CreateGameContentManager("(temporary)"); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + // default English + if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalisedAssetName)) + return this.Cache.ContainsKey(normalisedAssetName); + + // translated + string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) + { + return localisable + ? this.Cache.ContainsKey(localeKey) + : this.Cache.ContainsKey(normalisedAssetName); + } + + // not loaded yet + return false; + } + + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) + { + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Value.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); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Key.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; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + } + + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + + // edit asset + foreach (var entry in this.GetInterceptors(this.Editors)) + { + // check for match + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // try edit + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // validate edit + if (asset.Data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + else if (!(asset.Data is T)) + { + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + } + + // 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/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs new file mode 100644 index 00000000..aa5be9b6 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files. + internal interface IContentManager : IDisposable + { + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + string Name { get; } + + /// The current language as a constant. + LocalizedContentManager.LanguageCode Language { get; } + + /// The absolute path to the . + string FullRootDirectory { get; } + + /// Whether this content manager is for a mod folder. + bool IsModContentManager { get; } + + + /********* + ** Methods + *********/ + /// 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. + T Load(string assetName); + + /// 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 language code for which to load content. + T Load(string assetName, LocalizedContentManager.LanguageCode language); + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + void Inject(string assetName, T value); + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + string NormalisePathSeparators(string path); + + /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + string AssertAndNormaliseAssetName(string assetName); + + /// Get the current content locale. + string GetLocale(); + + /// The locale for a language. + /// The language. + string GetLocale(LocalizedContentManager.LanguageCode language); + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + bool IsLoaded(string assetName); + + /// Get the cached asset keys. + IEnumerable GetAssetKeys(); + + /// 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 the number of invalidated assets. + IEnumerable InvalidateCache(Func predicate, bool dispose = false); + } +} diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs new file mode 100644 index 00000000..80bf37e9 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -0,0 +1,207 @@ +using System; +using System.Globalization; +using System.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + internal class ModContentManager : BaseContentManager + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) { } + + /// 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 language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + + // get from cache + if (this.IsLoaded(assetName)) + return base.Load(assetName, language); + + // get managed asset + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + if (contentManagerID != this.Name) + { + T data = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); + this.Inject(assetName, data); + return data; + } + + return this.LoadManagedAsset(assetName, contentManagerID, relativePath, language); + } + + throw new NotSupportedException("Can't load content folder asset from a mod content manager."); + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + throw new NotSupportedException("Can't create a temporary mod content manager."); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName); + } + + /// Load a managed mod asset. + /// The type of asset to load. + /// The internal asset key. + /// The unique name for the content manager which should load this asset. + /// The relative path within the mod folder. + /// The language code for which to load content. + private T LoadManagedAsset(string internalKey, string contentManagerID, string relativePath, LanguageCode language) + { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{relativePath}' from {contentManagerID}: {reasonPhrase}"); + try + { + // get file + FileInfo file = this.GetModFile(relativePath); + if (!file.Exists) + throw GetContentError("the specified path doesn't exist."); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + return base.Load(relativePath, language); + + // unpacked map + case ".tbin": + throw GetContentError($"can't read unpacked map file directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.Inject(internalKey, texture); + return (T)(object)texture; + } + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") + throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); + throw new SContentLoadException($"The content manager failed loading content asset '{relativePath}' from {contentManagerID}.", ex); + } + } + + /// Get a file from the mod folder. + /// The asset path relative to the content folder. + private FileInfo GetModFile(string path) + { + // try exact match + FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(file.FullName + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4a71f7e7..ce26c980 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Utilities; using StardewValley; @@ -25,8 +26,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// SMAPI's core content logic. private readonly ContentCoordinator ContentCore; - /// The content manager for this mod. - private readonly SContentManager ContentManager; + /// A content manager for this mod which manages files from the game's Content folder. + private readonly IContentManager GameContentManager; + + /// A content manager for this mod which manages files from the mod's folder. + private readonly IContentManager ModContentManager; /// The absolute path to the mod folder. private readonly string ModFolderPath; @@ -42,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Accessors *********/ /// The game's current locale code (like pt-BR). - public string CurrentLocale => this.ContentManager.GetLocale(); + public string CurrentLocale => this.GameContentManager.GetLocale(); /// The game's current locale as an enum value. - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.Language; + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); @@ -65,16 +69,16 @@ namespace StardewModdingAPI.Framework.ModHelpers *********/ /// Construct an instance. /// SMAPI's core content logic. - /// The content manager for this mod. /// The absolute path to the mod folder. /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. /// Encapsulates monitoring and logging. - public ContentHelper(ContentCoordinator contentCore, SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentCore = contentCore; - this.ContentManager = contentManager; + this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content"); + this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), rootDirectory: modFolderPath); this.ModFolderPath = modFolderPath; this.ModName = modName; this.Monitor = monitor; @@ -92,24 +96,22 @@ namespace StardewModdingAPI.Framework.ModHelpers try { - this.AssertValidAssetKeyFormat(key); + this.AssertAndNormaliseAssetName(key); switch (source) { case ContentSource.GameContent: - return this.ContentCore.MainContentManager.Load(key); + return this.GameContentManager.Load(key); case ContentSource.ModFolder: // get file FileInfo file = this.GetModFile(key); if (!file.Exists) throw GetContentError($"there's no matching file at path '{file.FullName}'."); - - // get asset path - string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.ModFolder); + string internalKey = this.GetInternalModAssetKey(file); // try cache - if (this.ContentManager.IsLoaded(assetName)) - return this.ContentManager.Load(assetName); + if (this.ModContentManager.IsLoaded(internalKey)) + return this.ModContentManager.Load(internalKey); // fix map tilesheets if (file.Extension.ToLower() == ".tbin") @@ -121,15 +123,15 @@ namespace StardewModdingAPI.Framework.ModHelpers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - this.FixCustomTilesheetPaths(map, key); + this.FixCustomTilesheetPaths(map, relativeMapPath: key); // inject map - this.ContentManager.Inject(assetName, map); + this.ModContentManager.Inject(internalKey, map); return (T)(object)map; } // load through content manager - return this.ContentManager.Load(assetName); + return this.ModContentManager.Load(internalKey); default: throw GetContentError($"unknown content source '{source}'."); @@ -146,7 +148,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [Pure] public string NormaliseAssetName(string assetName) { - return this.ContentManager.NormaliseAssetName(assetName); + return this.ModContentManager.AssertAndNormaliseAssetName(assetName); } /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. @@ -158,11 +160,11 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentManager.NormaliseAssetName(key); + return this.GameContentManager.AssertAndNormaliseAssetName(key); case ContentSource.ModFolder: FileInfo file = this.GetModFile(key); - return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName, ContentSource.GameContent)); + return this.GetInternalModAssetKey(file); default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -205,16 +207,24 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The asset key to check. /// The asset key is empty or contains invalid characters. [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertValidAssetKeyFormat(string key) + private void AssertAndNormaliseAssetName(string key) { - this.ContentManager.AssertValidAssetKeyFormat(key); + this.ModContentManager.AssertAndNormaliseAssetName(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } + /// Get the internal key in the content cache for a mod asset. + /// The asset file. + private string GetInternalModAssetKey(FileInfo modFile) + { + string relativePath = PathUtilities.GetRelativePath(this.ModFolderPath, modFile.FullName); + return Path.Combine(this.ModContentManager.Name, relativePath); + } + /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. - /// The map asset key within the mod folder. + /// The relative map path within the mod folder. /// A map tilesheet couldn't be resolved. /// /// The game's logic for tilesheets in is a bit specialised. It boils @@ -230,13 +240,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. /// - private void FixCustomTilesheetPaths(Map map, string mapKey) + private void FixCustomTilesheetPaths(Map map, string relativeMapPath) { // get 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 + relativeMapPath = this.ModContentManager.AssertAndNormaliseAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets foreach (TileSheet tilesheet in map.TileSheets) @@ -341,7 +351,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetModFile(string path) { // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, this.ModContentManager.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); // try with default extension @@ -360,7 +370,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetContentFolderFile(string key) { // get file path - string path = Path.Combine(this.ContentManager.FullRootDirectory, key); + string path = Path.Combine(this.GameContentManager.FullRootDirectory, key); if (!path.EndsWith(".xnb")) path += ".xnb"; diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs deleted file mode 100644 index e97e655d..00000000 --- a/src/SMAPI/Framework/SContentManager.cs +++ /dev/null @@ -1,653 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.IO; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A minimal content manager which defers to SMAPI's core content logic. - internal class SContentManager : LocalizedContentManager - { - /********* - ** Properties - *********/ - /// The central coordinator which manages content managers. - private readonly ContentCoordinator Coordinator; - - /// The underlying asset cache. - private readonly ContentCache Cache; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. - private readonly IDictionary IsLocalisableLookup; - - /// The language enum values indexed by locale code. - private readonly IDictionary LanguageCodes; - - /// 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(); - - /// The path prefix for assets in mod folders. - private readonly string ModContentPrefix; - - /// A callback to invoke when the content manager is being disposed. - private readonly Action OnDisposing; - - /// Interceptors which provide the initial versions of matching assets. - private IDictionary> Loaders => this.Coordinator.Loaders; - - /// Interceptors which edit matching assets after they're loaded. - private IDictionary> Editors => this.Coordinator.Editors; - - /// Whether the content coordinator has been disposed. - private bool IsDisposed; - - - /********* - ** Accessors - *********/ - /// A name for the mod manager. Not guaranteed to be unique. - public string Name { get; } - - /// Whether this content manager is wrapped around a mod folder. - public bool IsModFolder { get; } - - /// The current language as a constant. - public LocalizedContentManager.LanguageCode Language => this.GetCurrentLanguage(); - - /// The absolute path to the . - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A name for the mod manager. Not guaranteed to be unique. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - /// The central coordinator which manages content managers. - /// Encapsulates monitoring and logging. - /// Simplifies access to private code. - /// Whether this content manager is wrapped around a mod folder. - /// A callback to invoke when the content manager is being disposed. - public SContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) - : base(serviceProvider, rootDirectory, currentCulture) - { - // init - this.Name = name; - this.IsModFolder = isModFolder; - this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); - this.Cache = new ContentCache(this, reflection); - this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath, ContentSource.GameContent); - this.OnDisposing = onDisposing; - - // get asset data - this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); - this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); - - } - - /// 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.Load(assetName, LocalizedContentManager.CurrentLanguageCode); - } - - /// 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 language code for which to load content. - public override T Load(string assetName, LanguageCode language) - { - // normalise asset key - this.AssertValidAssetKeyFormat(assetName); - assetName = this.NormaliseAssetName(assetName); - - // load game content - if (!this.IsModFolder && !assetName.StartsWith(this.ModContentPrefix)) - return this.LoadImpl(assetName, language); - - // load mod content - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); - try - { - // try cache - if (this.IsLoaded(assetName)) - return this.LoadImpl(assetName, language); - - // get file - FileInfo file = this.GetModFile(assetName); - if (!file.Exists) - throw GetContentError("the specified path doesn't exist."); - - // load content - switch (file.Extension.ToLower()) - { - // XNB file - case ".xnb": - return this.LoadImpl(assetName, language); - - // unpacked map - case ".tbin": - throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); - - // unpacked image - case ".png": - // validate - if (typeof(T) != typeof(Texture2D)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - - // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - this.Inject(assetName, texture); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); - } - } - catch (Exception ex) when (!(ex is SContentLoadException)) - { - if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") - throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); - throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); - } - } - - /// Load the base asset without localisation. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T LoadBase(string assetName) - { - return this.Load(assetName, LanguageCode.en); - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - public void Inject(string assetName, T value) - { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - } - - /// Create a new content manager for temporary use. - public override LocalizedContentManager CreateTemporary() - { - return this.Coordinator.CreateContentManager("(temporary)", isModFolder: false); - } - - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. - [Pure] - public string NormalisePathSeparators(string path) - { - return this.Cache.NormalisePathSeparators(path); - } - - /// Normalise an asset name so it's consistent with the underlying cache. - /// The asset key. - [Pure] - public string NormaliseAssetName(string assetName) - { - return this.Cache.NormaliseKey(assetName); - } - - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - public void AssertValidAssetKeyFormat(string key) - { - // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid - // throwing other types like ArgumentException here. - if (string.IsNullOrWhiteSpace(key)) - throw new SContentLoadException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new SContentLoadException("The asset key or local path contains invalid characters."); - } - - /// Convert an absolute file path into an appropriate asset name. - /// The absolute path to the file. - /// The folder to which to get a relative path. - public string GetAssetNameFromFilePath(string absolutePath, ContentSource relativeTo) - { -#if SMAPI_FOR_WINDOWS - // XNA doesn't allow absolute asset paths, so get a path relative to the source folder - string sourcePath = relativeTo == ContentSource.GameContent ? this.Coordinator.FullRootDirectory : this.FullRootDirectory; - return this.GetRelativePath(sourcePath, absolutePath); -#else - // MonoGame is weird about relative paths on Mac, but allows absolute paths - return absolutePath; -#endif - } - - /**** - ** Content loading - ****/ - /// Get the current content locale. - public string GetLocale() - { - return this.GetLocale(this.GetCurrentLanguage()); - } - - /// The locale for a language. - /// The language. - public string GetLocale(LocalizedContentManager.LanguageCode language) - { - return this.LanguageCodeString(language); - } - - /// Get whether the content manager has already loaded and cached the given asset. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool IsLoaded(string assetName) - { - assetName = this.Cache.NormaliseKey(assetName); - return this.IsNormalisedKeyLoaded(assetName); - } - - /// Get the cached asset keys. - public IEnumerable GetAssetKeys() - { - return this.Cache.Keys - .Select(this.GetAssetName) - .Distinct(); - } - - /**** - ** Cache invalidation - ****/ - /// 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 the number of invalidated assets. - public IEnumerable InvalidateCache(Func predicate, bool dispose = false) - { - HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => - { - this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) - { - removeAssetNames.Add(assetName); - return true; - } - return false; - }); - - return removeAssetNames; - } - - /// Dispose held resources. - /// Whether the content manager is being disposed (rather than finalized). - protected override void Dispose(bool isDisposing) - { - if (this.IsDisposed) - return; - this.IsDisposed = true; - - this.OnDisposing(this); - base.Dispose(isDisposing); - } - - /// - public override void Unload() - { - if (this.IsDisposed) - return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading - - base.Unload(); - } - - - /********* - ** Private methods - *********/ - /**** - ** Asset name/key handling - ****/ - /// Get a directory or file path relative to the content root. - /// The source file path. - /// The target file path. - private string GetRelativePath(string sourcePath, string targetPath) - { - return PathUtilities.GetRelativePath(sourcePath, targetPath); - } - - /// Get the locale codes (like ja-JP) used in asset keys. - private IDictionary GetKeyLocales() - { - // create locale => code map - IDictionary map = new Dictionary(); - foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) - map[code] = this.GetLocale(code); - - return map; - } - - /// Get the asset name from a cache key. - /// The input cache key. - private string GetAssetName(string cacheKey) - { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; - } - - /// Parse a cache key into its component parts. - /// The input cache key. - /// The original asset name. - /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) - { - // handle localised key - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); - if (lastSepIndex >= 0) - { - string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.LanguageCodes.ContainsKey(suffix)) - { - assetName = cacheKey.Substring(0, lastSepIndex); - localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - return; - } - } - } - - // handle simple key - assetName = cacheKey; - localeCode = null; - } - - /**** - ** Cache handling - ****/ - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - // default English - if (this.Language == LocalizedContentManager.LanguageCode.en) - return this.Cache.ContainsKey(normalisedAssetName); - - // translated - string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) - { - return localisable - ? this.Cache.ContainsKey(localeKey) - : this.Cache.ContainsKey(normalisedAssetName); - } - - // not loaded yet - return false; - } - - /**** - ** Content loading - ****/ - /// Load an asset name without heuristics to support mod content. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - private T LoadImpl(string assetName, LocalizedContentManager.LanguageCode language) - { - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - return base.Load(assetName, language); - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load(assetName, language); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => - { - string locale = this.GetLocale(language); - IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = - this.ApplyLoader(info) - ?? new AssetDataForObject(info, base.Load(assetName, language), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } - - // update cache & return data - this.Inject(assetName, data); - return data; - } - - /// Get a file from the mod folder. - /// The asset path relative to the content folder. - private FileInfo GetModFile(string path) - { - // try exact match - FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); - - // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") - { - FileInfo result = new FileInfo(file.FullName + ".xnb"); - if (result.Exists) - file = result; - } - - return file; - } - - /// Load the initial asset from the registered . - /// The basic asset metadata. - /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData ApplyLoader(IAssetInfo info) - { - // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) - .Where(entry => - { - try - { - return entry.Value.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); - return false; - } - }) - .ToArray(); - - // validate loaders - if (!loaders.Any()) - return null; - if (loaders.Length > 1) - { - string[] loaderNames = loaders.Select(p => p.Key.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; - T data; - try - { - data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return null; - } - - // validate asset - if (data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); - return null; - } - - // return matched asset - return new AssetDataForObject(info, data, this.NormaliseAssetName); - } - - /// Apply any to a loaded asset. - /// The asset type. - /// The basic asset metadata. - /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) - { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); - - // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) - { - // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; - try - { - if (!editor.CanEdit(info)) - continue; - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // try edit - object prevAsset = asset.Data; - try - { - editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // validate edit - if (asset.Data == null) - { - mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - else if (!(asset.Data is T)) - { - mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - } - - // 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); - } - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } - } -} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 8a4f987b..91612fb0 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -202,7 +202,7 @@ namespace StardewModdingAPI.Framework this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); SGame.MonitorDuringInitialisation = null; this.NextContentManagerIsMain = true; - return this.ContentCore.CreateContentManager("Game1._temporaryContent", isModFolder: false); + return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); } // Game1.content initialising from LoadContent @@ -213,7 +213,7 @@ namespace StardewModdingAPI.Framework } // any other content manager - return this.ContentCore.CreateContentManager("(generated)", isModFolder: false, rootDirectory: rootDirectory); + return this.ContentCore.CreateGameContentManager("(generated)"); } /// The method called when the game is updating its state. This happens roughly 60 times per second. diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs index 0233d796..51d45ebd 100644 --- a/src/SMAPI/Framework/Utilities/PathUtilities.cs +++ b/src/SMAPI/Framework/Utilities/PathUtilities.cs @@ -23,9 +23,12 @@ namespace StardewModdingAPI.Framework.Utilities *********/ /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). /// The path to split. - public static string[] GetSegments(string path) + /// The number of segments to match. Any additional segments will be merged into the last returned part. + public static string[] GetSegments(string path, int? limit = null) { - return path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + return limit.HasValue + ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) + : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); } /// Normalise path separators in a file path. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6aff6dc6..340d2ddb 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -755,8 +755,7 @@ namespace StardewModdingAPI // load mod as content pack IManifest manifest = metadata.Manifest; IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); - IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); metadata.SetMod(contentPack, monitor); this.ModRegistry.Add(metadata); @@ -844,8 +843,7 @@ namespace StardewModdingAPI IModHelper modHelper; { ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - SContentManager contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", isModFolder: true, rootDirectory: metadata.DirectoryPath); - IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); @@ -854,8 +852,7 @@ namespace StardewModdingAPI IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - SContentManager packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", isModFolder: true, rootDirectory: packDirPath); - IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 19c1e6fe..e9e0ea54 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -87,6 +87,10 @@
+ + + + @@ -123,7 +127,6 @@ - -- cgit From d9c6015163a3a20cc7e84c512efe04b6d210ac65 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 23 May 2018 00:19:22 -0400 Subject: exclude common non-mod files from mod release zips --- docs/release-notes.md | 1 + .../Framework/ModFileManager.cs | 27 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 118cc441..86ba5e49 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -32,6 +32,7 @@ * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. + * Fixed some common non-mod build output being included in release zip. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten (see above). diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index ba2e671d..3fec8215 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -73,11 +73,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework continue; // ignore release zips - if (this.EqualsInvariant(file.Extension, ".zip")) - continue; - - // ignore Json.NET (bundled into SMAPI) - if (this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")) + if (this.ShouldIgnore(file)) continue; // add file @@ -145,6 +141,27 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /********* ** Private methods *********/ + /// Get whether a build output file should be ignored. + /// The file info. + private bool ShouldIgnore(FileInfo file) + { + return + // release zips + this.EqualsInvariant(file.Extension, ".zip") + + // Json.NET (bundled into SMAPI) + || this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") + || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml") + + // code analysis files + || file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.InvariantCultureIgnoreCase) + || file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.InvariantCultureIgnoreCase) + + // OS metadata files + || this.EqualsInvariant(file.Name, ".DS_Store") + || this.EqualsInvariant(file.Name, "Thumbs.db"); + } + /// Get a case-insensitive dictionary matching the given JSON. /// The JSON to parse. private IDictionary Parse(string json) -- cgit From b942c89dcf5b9e150c9aef51edac79d0890b8b9e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 23 May 2018 00:35:43 -0400 Subject: fix launch issue for Linux players with some terminals (#489, #526) --- docs/release-notes.md | 1 + src/SMAPI.Installer/unix-launcher.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 86ba5e49..24665638 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * Fixed `smapi.io/install` not linking to a useful page. * Fixed `world_setseason` command not running season-change logic. * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. + * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index f16cc317..1e969c20 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -74,9 +74,9 @@ else elif $COMMAND xterm 2>/dev/null; then xterm -e "$LAUNCHER" elif $COMMAND xfce4-terminal 2>/dev/null; then - xfce4-terminal -e "$LAUNCHER" + xfce4-terminal -e "env TERM=xterm; $LAUNCHER" elif $COMMAND gnome-terminal 2>/dev/null; then - gnome-terminal -e "$LAUNCHER" + gnome-terminal -e "env TERM=xterm; $LAUNCHER" elif $COMMAND konsole 2>/dev/null; then konsole -p Environment=TERM=xterm -e "$LAUNCHER" elif $COMMAND terminal 2>/dev/null; then -- cgit From fda2ac74857e3bc1129667ef6ba975efd6ca33f6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 23 May 2018 00:50:46 -0400 Subject: reorganise SaveBackup code a bit (#522) --- src/SMAPI.Mods.SaveBackup/ModEntry.cs | 67 ++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index e9e62752..37793ced 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; @@ -27,36 +26,15 @@ namespace StardewModdingAPI.Mods.SaveBackup { try { - // read config ModConfig config = this.Helper.ReadConfig(); // init backup folder - DirectoryInfo folder = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "backups")); - folder.Create(); + DirectoryInfo backupFolder = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "backups")); + backupFolder.Create(); // back up saves - { - FileInfo file = new FileInfo(Path.Combine(folder.FullName, this.FileName)); - if (!file.Exists) - { - this.Monitor.Log($"Adding {file.Name}...", LogLevel.Trace); - ZipFile.CreateFromDirectory(Constants.SavesPath, file.FullName, CompressionLevel.Fastest, includeBaseDirectory: false); - } - } - - // prune old saves - foreach (FileInfo file in this.GetOldBackups(folder, config.BackupsToKeep)) - { - try - { - this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); - file.Delete(); - } - catch (Exception ex) - { - this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}"); - } - } + this.CreateBackup(backupFolder); + this.PruneBackups(backupFolder, config.BackupsToKeep); } catch (Exception ex) { @@ -68,15 +46,40 @@ namespace StardewModdingAPI.Mods.SaveBackup /********* ** Private methods *********/ - /// Get backups ordered by creation date. - /// The folder to search. - /// The number of backups to skip. - private IEnumerable GetOldBackups(DirectoryInfo folder, int skip) + /// Back up the current saves. + /// The folder containing save backups. + private void CreateBackup(DirectoryInfo backupFolder) { - return folder + FileInfo file = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName)); + if (!file.Exists) + { + this.Monitor.Log($"Adding {file.Name}...", LogLevel.Trace); + ZipFile.CreateFromDirectory(Constants.SavesPath, file.FullName, CompressionLevel.Fastest, includeBaseDirectory: false); + } + } + + /// Remove old backups if we've exceeded the limit. + /// The folder containing save backups. + /// The number of backups to keep. + private void PruneBackups(DirectoryInfo backupFolder, int backupsToKeep) + { + var oldBackups = backupFolder .GetFiles() .OrderByDescending(p => p.CreationTimeUtc) - .Skip(skip); + .Skip(backupsToKeep); + + foreach (FileInfo file in oldBackups) + { + try + { + this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); + file.Delete(); + } + catch (Exception ex) + { + this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}"); + } + } } } } -- cgit From da22446964f4dba0047a29dee3efec7c00835048 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 23 May 2018 19:09:19 -0400 Subject: fix SaveBackup failing on Mac (#522) --- src/SMAPI.Mods.SaveBackup/ModEntry.cs | 82 +++++++++++++++++----- .../StardewModdingAPI.Mods.SaveBackup.csproj | 2 - 2 files changed, 65 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index 37793ced..78578c3c 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Reflection; using StardewModdingAPI.Mods.SaveBackup.Framework; using StardewValley; @@ -50,11 +52,49 @@ namespace StardewModdingAPI.Mods.SaveBackup /// The folder containing save backups. private void CreateBackup(DirectoryInfo backupFolder) { - FileInfo file = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName)); - if (!file.Exists) + try + { + // get target path + FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName)); + if (targetFile.Exists) + targetFile.Delete(); //return; + + // create zip + // due to limitations with the bundled Mono on Mac, we can't reference System.IO.Compression. + this.Monitor.Log($"Adding {targetFile.Name}...", LogLevel.Trace); + switch (Constants.TargetPlatform) + { + case GamePlatform.Linux: + case GamePlatform.Windows: + { + Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly."); + Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly."); + Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type."); + Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type."); + MethodInfo createMethod = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method."); + createMethod.Invoke(null, new object[] { Constants.SavesPath, targetFile.FullName, CompressionLevel.Fastest, false }); + } + break; + + case GamePlatform.Mac: + { + DirectoryInfo saveFolder = new DirectoryInfo(Constants.SavesPath); + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "zip", + Arguments = $"-rq \"{targetFile.FullName}\" \"{saveFolder.Name}\" -x \"*.DS_Store\" -x \"__MACOSX\"", + WorkingDirectory = $"{Constants.SavesPath}/../", + CreateNoWindow = true + }; + new Process { StartInfo = startInfo }.Start(); + } + break; + } + } + catch (Exception ex) { - this.Monitor.Log($"Adding {file.Name}...", LogLevel.Trace); - ZipFile.CreateFromDirectory(Constants.SavesPath, file.FullName, CompressionLevel.Fastest, includeBaseDirectory: false); + this.Monitor.Log("Couldn't back up save files (see log file for details).", LogLevel.Warn); + this.Monitor.Log(ex.ToString(), LogLevel.Trace); } } @@ -63,23 +103,31 @@ namespace StardewModdingAPI.Mods.SaveBackup /// The number of backups to keep. private void PruneBackups(DirectoryInfo backupFolder, int backupsToKeep) { - var oldBackups = backupFolder - .GetFiles() - .OrderByDescending(p => p.CreationTimeUtc) - .Skip(backupsToKeep); - - foreach (FileInfo file in oldBackups) + try { - try - { - this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); - file.Delete(); - } - catch (Exception ex) + var oldBackups = backupFolder + .GetFiles() + .OrderByDescending(p => p.CreationTimeUtc) + .Skip(backupsToKeep); + + foreach (FileInfo file in oldBackups) { - this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}"); + try + { + this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); + file.Delete(); + } + catch (Exception ex) + { + this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}"); + } } } + catch (Exception ex) + { + this.Monitor.Log("Couldn't remove old backups (see log file for details).", LogLevel.Warn); + this.Monitor.Log(ex.ToString(), LogLevel.Trace); + } } } } diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index 89e92a8a..44fff536 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -31,8 +31,6 @@ - - -- cgit From 93274deb4aba6eb60b5471a81819eb92e23de30b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 18:15:02 -0400 Subject: minor fixes --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 6 +++--- src/SMAPI/IAssetDataForImage.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 1eef2afb..5c7b87de 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics; namespace StardewModdingAPI.Framework.Content { - /// Encapsulates access and changes to dictionary content being read from a data file. + /// Encapsulates access and changes to image content being read from a data file. internal class AssetDataForImage : AssetData, IAssetDataForImage { /********* @@ -29,6 +29,8 @@ namespace StardewModdingAPI.Framework.Content public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { // get texture + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); Texture2D target = this.Data; // get areas @@ -36,8 +38,6 @@ namespace StardewModdingAPI.Framework.Content targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate - if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs index 4584a20e..1109194f 100644 --- a/src/SMAPI/IAssetDataForImage.cs +++ b/src/SMAPI/IAssetDataForImage.cs @@ -1,10 +1,10 @@ -using System; +using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace StardewModdingAPI { - /// Encapsulates access and changes to dictionary content being read from a data file. + /// Encapsulates access and changes to image content being read from a data file. public interface IAssetDataForImage : IAssetData { /********* -- cgit From 80ff10c5ccbaec29f35dc720e254218469157b6b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 18:33:56 -0400 Subject: add option to locally suppress update checks for a specific mod --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/SConfig.cs | 3 +++ src/SMAPI/Program.cs | 16 +++++++--------- src/SMAPI/StardewModdingAPI.config.json | 10 +++++++++- 4 files changed, 20 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 24665638..e4feffd4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -34,6 +34,7 @@ * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * Fixed some common non-mod build output being included in release zip. + * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten (see above). diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index e201e966..98614933 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -28,5 +28,8 @@ namespace StardewModdingAPI.Framework.Models /// The console color scheme to use. public MonitorColorScheme ColorScheme { get; set; } + + /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. + public string[] SuppressUpdateChecks { get; set; } } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6aff6dc6..2dcf67e4 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -97,13 +97,6 @@ namespace StardewModdingAPI new Regex(@"^DebugOutput: added CLOUD", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; - /// The mod IDs for which to not show missing update key warnings. - private readonly string[] AllowMissingUpdateKeys = - { - "SMAPI.ConsoleCommands", - "SMAPI.SaveBackup" - }; - /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper = new JsonHelper(); @@ -615,11 +608,15 @@ namespace StardewModdingAPI { try { + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); + // prepare update keys Dictionary modsByKey = ( from mod in mods - where mod.Manifest?.UpdateKeys != null + where + mod.Manifest?.UpdateKeys != null + && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) from key in mod.Manifest.UpdateKeys select new { key, mod } ) @@ -732,6 +729,7 @@ namespace StardewModdingAPI { this.Monitor.Log("Loading mods...", LogLevel.Trace); + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); IDictionary skippedMods = new Dictionary(); void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase }; @@ -790,7 +788,7 @@ namespace StardewModdingAPI : $" {metadata.DisplayName}...", LogLevel.Trace); // show warnings - if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !this.AllowMissingUpdateKeys.Contains(metadata.Manifest.UniqueID)) + if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) metadata.SetWarning(ModWarning.NoUpdateKeys); // validate status diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 6725dbbd..7aac6e4c 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -50,5 +50,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * - LightBackground: use darker text colors that look better on a white or light background. * - DarkBackground: use lighter text colors that look better on a black or dark background. */ - "ColorScheme": "AutoDetect" + "ColorScheme": "AutoDetect", + + /** + * The mod IDs SMAPI should ignore when performing update checks or validating update keys. + */ + "SuppressUpdateChecks": [ + "SMAPI.ConsoleCommands", + "SMAPI.SaveBackup" + ] } -- cgit From a059da747a4ebf5cc4257b449a85031f7842c291 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 19:57:00 -0400 Subject: fix input suppression not working on the title menu (#527) --- src/SMAPI/Framework/Input/SInputState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 5e8efa62..27e40ab4 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Framework.Input /// Whether input should be suppressed in the current context. private bool ShouldSuppressNow() { - return Game1.chatBox != null && !Game1.chatBox.isActive(); + return Game1.chatBox == null || !Game1.chatBox.isActive(); } /// Apply input suppression to the given input states. -- cgit From 16ad205ded9b0defe939213b57948216f43f948f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 20:40:57 -0400 Subject: suppress more game debug output (#511) --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2dcf67e4..f4768389 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^Multiplayer auth success$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^DebugOutput: added CLOUD", RegexOptions.Compiled | RegexOptions.CultureInvariant) + new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; /// Encapsulates SMAPI's JSON file parsing. -- cgit From 47ab534bee74904e2ae84ef569f65b4e26623059 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 22:24:30 -0400 Subject: tweak context trace messages to make multiplayer context more clear --- src/SMAPI/Framework/SGame.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 91612fb0..585b3270 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -386,8 +386,19 @@ namespace StardewModdingAPI.Framework *********/ if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) { + // print context + string context = $"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}."; + if (Context.IsMultiplayer) + { + int onlineCount = Game1.getOnlineFarmers().Count(); + context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online."; + } + else + context += " Single-player."; + this.Monitor.Log(context, LogLevel.Trace); + + // raise events this.RaisedAfterLoadEvent = true; - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); this.Events.Save_AfterLoad.Raise(); this.Events.Time_AfterDayStarted.Raise(); } -- cgit From d2ea678cc703ead74b6c97727ca094d13c9cc272 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 22:31:24 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 50d25697..d75b42d5 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.14", + "Version": "2.6.0-beta.15", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 86f85e26..d076d84f 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.14", + "Version": "2.6.0-beta.15", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index fc295f28..bdbd5fbb 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.14"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.15"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.11"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.13"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 4e102cb27477778d1108ad67ca28af4b01652301 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:06:35 -0400 Subject: suppress implicit cast warnings in code mirrored from game --- src/SMAPI/Framework/SGame.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 585b3270..369f1f40 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -739,6 +739,7 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] private void DrawImpl(GameTime gameTime) { if (Game1.debugMode) -- cgit From 2a7bcb28f6a390e165c2cb8817f333aad750a032 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:13:28 -0400 Subject: add empty toolkit project & fix misleading build configuration name (#532) --- build/common.targets | 8 ++- build/prepare-install-package.targets | 8 ++- src/SMAPI.sln | 82 ++++++++++++---------- src/SMAPI/StardewModdingAPI.csproj | 6 ++ .../Properties/AssemblyInfo.cs | 6 ++ .../StardewModdingAPI.Toolkit.csproj | 22 ++++++ 6 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj (limited to 'src') diff --git a/build/common.targets b/build/common.targets index b382ea54..2ee24e52 100644 --- a/build/common.targets +++ b/build/common.targets @@ -25,7 +25,7 @@ - + @@ -95,7 +95,7 @@ - + @@ -106,6 +106,10 @@ + + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 433078d7..9a5cde3f 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -7,7 +7,9 @@ --> - $(SolutionDir)\..\bin\$(Configuration)\SMAPI + $(SolutionDir)\..\bin\$(Configuration) + $(CompiledRootPath)\SMAPI + $(CompiledRootPath)\SMAPI.Toolkit\net4.5 $(SolutionDir)\..\bin\Packaged $(PackagePath)\internal @@ -35,6 +37,8 @@ + + @@ -46,6 +50,8 @@ + + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index dec26694..0eb42cce 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -60,6 +60,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\StardewModdingAPI.Mods.SaveBackup.csproj", "{E272EB5D-8C57-417E-8E60-C1079D3F53C4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 @@ -68,46 +70,50 @@ Global SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 + Debug|Default = Debug|Default + Release|Default = Release|Default EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86 - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.Build.0 = Release|Any CPU - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|x86.ActiveCfg = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|x86.Build.0 = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|x86.ActiveCfg = Release|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|x86.Build.0 = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.ActiveCfg = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.Build.0 = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.Build.0 = Release|Any CPU + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.Build.0 = Release|x86 + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.ActiveCfg = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.Build.0 = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.ActiveCfg = Release|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.Build.0 = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.ActiveCfg = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.Build.0 = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.ActiveCfg = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.Build.0 = Release|Any CPU + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.ActiveCfg = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.Build.0 = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.ActiveCfg = Release|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.Build.0 = Release|x86 + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.ActiveCfg = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.Build.0 = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.ActiveCfg = Release|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e9e0ea54..926bcbbc 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -314,6 +314,12 @@ + + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + + diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..20118fce --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("SMAPI.Toolkit")] +[assembly: AssemblyDescription("")] +[assembly: InternalsVisibleTo("StardewModdingAPI")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj new file mode 100644 index 00000000..ffda899a --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -0,0 +1,22 @@ + + + + net4.5;netstandard2.0 + false + + + + ..\..\bin\$(Configuration)\SMAPI.Toolkit + + + + + + + + + + + + + -- cgit From 3129f67eb1f162e06d96854f319b10ccf583f0aa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:14:40 -0400 Subject: add semantic version to toolkit (#532) --- src/SMAPI.Installer/InteractiveInstaller.cs | 12 +- src/SMAPI.Internal/SMAPI.Internal.projitems | 1 - src/SMAPI.Internal/SemanticVersionImpl.cs | 199 ----------------- .../Framework/ModFileManager.cs | 6 +- .../StardewModdingAPI.ModBuildConfig.csproj | 6 + src/SMAPI.Web/Controllers/IndexController.cs | 8 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 4 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 4 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 3 + src/SMAPI/SemanticVersion.cs | 16 +- src/StardewModdingAPI.Toolkit/ISemanticVersion.cs | 46 ++++ .../Properties/AssemblyInfo.cs | 1 + src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 239 +++++++++++++++++++++ 13 files changed, 325 insertions(+), 220 deletions(-) delete mode 100644 src/SMAPI.Internal/SemanticVersionImpl.cs create mode 100644 src/StardewModdingAPI.Toolkit/ISemanticVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit/SemanticVersion.cs (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index ace560e5..6e4cb95d 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -152,7 +152,7 @@ namespace StardewModdingApi.Installer ** Get platform & set window title ****/ Platform platform = EnvironmentUtility.DetectPlatform(); - Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; + Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.WriteLine(); #if SMAPI_FOR_WINDOWS @@ -421,6 +421,16 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ + /// Get the display text for an assembly version. + /// The assembly version. + private string GetDisplayVersion(Version version) + { + string str = $"{version.Major}.{version.Minor}"; + if (version.Build != 0) + str += $".{version.Build}"; + return str; + } + /// Get the value of a key in the Windows registry. /// The full path of the registry key relative to HKLM. /// The name of the value. diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index dadae4b0..33b8cbfa 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -16,6 +16,5 @@ - \ No newline at end of file diff --git a/src/SMAPI.Internal/SemanticVersionImpl.cs b/src/SMAPI.Internal/SemanticVersionImpl.cs deleted file mode 100644 index 7ae34f07..00000000 --- a/src/SMAPI.Internal/SemanticVersionImpl.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI.Internal -{ - /// A low-level implementation of a semantic version with an optional release tag. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). - internal class SemanticVersionImpl : IComparable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - public int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } - - /// An optional prerelease tag. - public string Tag { get; } - - /// 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*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; - - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - /********* - ** Public methods - *********/ - /// 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. - public SemanticVersionImpl(int major, int minor, int patch, string tag = null) - { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); - } - - /// Construct an instance. - /// The assembly version. - /// The is null. - public SemanticVersionImpl(Version version) - { - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version can't be null."); - - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; - } - - /// Construct an instance. - /// The semantic version string. - /// The is null. - /// The is not a valid semantic version. - public SemanticVersionImpl(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersionImpl.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. - public int CompareTo(SemanticVersionImpl other) - { - if (other == null) - throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); - } - - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// 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 prerelease tag to compare with this instance. - public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) - { - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); - bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); - string[] otherParts = otherTag.Split('.', '-'); - for (int i = 0; i < curParts.Length; i++) - { - // longer prerelease tag supercedes if otherwise equal - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - /// Get a string representation of the version. - public override string ToString() - { - // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; - - // tag - string tag = this.Tag; - if (tag != null) - result += $"-{tag}"; - return result; - } - - /// Parse a version string without throwing an exception if it fails. - /// The version string. - /// The parsed representation. - /// Returns whether parsing the version succeeded. - internal static bool TryParse(string version, out SemanticVersionImpl parsed) - { - try - { - parsed = new SemanticVersionImpl(version); - return true; - } - catch - { - parsed = null; - return false; - } - } - - - /********* - ** Private methods - *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) - { - tag = tag?.Trim(); - return !string.IsNullOrWhiteSpace(tag) ? tag : null; - } - } -} diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 3fec8215..41e0201d 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Script.Serialization; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.ModBuildConfig.Framework { @@ -132,9 +132,9 @@ namespace StardewModdingAPI.ModBuildConfig.Framework int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; - return new SemanticVersionImpl(major, minor, patch, tag).ToString(); + return new SemanticVersion(major, minor, patch, tag).ToString(); } - return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ + return new SemanticVersion(versionObj.ToString()).ToString(); // SMAPI 2.0+ } diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index e0ea876f..6a52daac 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -55,6 +55,12 @@ + + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + + diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 08b7363a..0cc3c37a 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Web.Controllers foreach (GitAsset asset in release.Assets) { Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); - if (!match.Success || !SemanticVersionImpl.TryParse(match.Groups["version"].Value, out SemanticVersionImpl version)) + if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; bool isBeta = version.Tag != null; bool isForDevs = match.Groups["forDevs"].Success; @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Web.Controllers public GitAsset Asset { get; } /// The SMAPI version. - public SemanticVersionImpl Version { get; } + public ISemanticVersion Version { get; } /// Whether this is a beta download. public bool IsBeta { get; } @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Web.Controllers /// The SMAPI version. /// Whether this is a beta download. /// Whether this is a 'for developers' download. - public ReleaseVersion(GitRelease release, GitAsset asset, SemanticVersionImpl version, bool isBeta, bool isForDevs) + public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isBeta, bool isForDevs) { this.Release = release; this.Asset = asset; diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 163176fd..c50e643a 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing @@ -31,7 +31,7 @@ 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(@"^ (?.+?) (?" + SemanticVersionImpl.UnboundedVersionPattern + @")(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: 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); diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index 1502f5d8..2d6ec603 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Routing.Constraints; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework { @@ -11,6 +11,6 @@ namespace StardewModdingAPI.Web.Framework *********/ /// Construct an instance. public VersionConstraint() - : base(SemanticVersionImpl.Regex) { } + : base(SemanticVersion.Regex) { } } } diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index e4678269..202a8376 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -23,5 +23,8 @@ + + + diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 9db1cf14..3ee3ccf3 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,5 @@ using System; using Newtonsoft.Json; -using StardewModdingAPI.Internal; namespace StardewModdingAPI { @@ -11,7 +10,7 @@ namespace StardewModdingAPI ** Properties *********/ /// The underlying semantic version implementation. - private readonly SemanticVersionImpl Version; + private readonly Toolkit.ISemanticVersion Version; /********* @@ -40,20 +39,20 @@ namespace StardewModdingAPI /// An optional build tag. [JsonConstructor] public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - : this(new SemanticVersionImpl(majorVersion, minorVersion, patchVersion, build)) { } + : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, build)) { } /// Construct an instance. /// The semantic version string. /// The is null. /// The is not a valid semantic version. public SemanticVersion(string version) - : this(new SemanticVersionImpl(version)) { } + : this(new Toolkit.SemanticVersion(version)) { } /// Construct an instance. /// The assembly version. /// The is null. public SemanticVersion(Version version) - : this(new SemanticVersionImpl(version)) { } + : this(new Toolkit.SemanticVersion(version)) { } /// Whether this is a pre-release version. public bool IsPrerelease() @@ -67,7 +66,8 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - return this.Version.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); + Toolkit.ISemanticVersion toolkitOther = new Toolkit.SemanticVersion(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); + return this.Version.CompareTo(toolkitOther); } /// Get whether this version is older than the specified version. @@ -137,7 +137,7 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. internal static bool TryParse(string version, out ISemanticVersion parsed) { - if (SemanticVersionImpl.TryParse(version, out SemanticVersionImpl versionImpl)) + if (Toolkit.SemanticVersion.TryParse(version, out Toolkit.ISemanticVersion versionImpl)) { parsed = new SemanticVersion(versionImpl); return true; @@ -153,7 +153,7 @@ namespace StardewModdingAPI *********/ /// Construct an instance. /// The underlying semantic version implementation. - private SemanticVersion(SemanticVersionImpl version) + private SemanticVersion(Toolkit.ISemanticVersion version) { this.Version = version; } diff --git a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs new file mode 100644 index 00000000..ca62d393 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs @@ -0,0 +1,46 @@ +using System; + +namespace StardewModdingAPI.Toolkit +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + int Patch { get; } + + /// An optional prerelease tag. + string Tag { get; } + + + /********* + ** Accessors + *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 20118fce..9b55dac6 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("")] [assembly: InternalsVisibleTo("StardewModdingAPI")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs new file mode 100644 index 00000000..bd85f990 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -0,0 +1,239 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Toolkit +{ + /// A semantic version with an optional release tag. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + public class SemanticVersion : ISemanticVersion + { + /********* + ** Properties + *********/ + /// 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*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; + + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + public int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + public int Patch { get; } + + /// An optional prerelease tag. + public string Tag { get; } + + + /********* + ** Public methods + *********/ + /// 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 fixes. + /// An optional prerelease tag. + public SemanticVersion(int major, int minor, int patch, string tag = null) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.Tag = this.GetNormalisedTag(tag); + } + + /// Construct an instance. + /// The assembly version. + /// The is null. + public SemanticVersion(Version version) + { + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version can't be null."); + + this.Major = version.Major; + this.Minor = version.Minor; + this.Patch = version.Build; + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersion(string version) + { + // 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) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.Major = int.Parse(match.Groups["major"].Value); + this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + public int CompareTo(ISemanticVersion other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(ISemanticVersion other) + { + return other != null && this.CompareTo(other) == 0; + } + + /// Whether this is a pre-release version. + public bool IsPrerelease() + { + return !string.IsNullOrWhiteSpace(this.Tag); + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + public bool IsOlderThan(ISemanticVersion other) + { + return this.CompareTo(other) < 0; + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + public bool IsNewerThan(ISemanticVersion other) + { + return this.CompareTo(other) > 0; + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + public bool IsBetween(ISemanticVersion min, ISemanticVersion max) + { + return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + } + + /// Get a string representation of the version. + public override string ToString() + { + // version + string result = this.Patch != 0 + ? $"{this.Major}.{this.Minor}.{this.Patch}" + : $"{this.Major}.{this.Minor}"; + + // tag + string tag = this.Tag; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// 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) + { + try + { + parsed = new SemanticVersion(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + return !string.IsNullOrWhiteSpace(tag) ? tag : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// 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 prerelease tag to compare with this instance. + private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.Major != otherMajor) + return this.Major.CompareTo(otherMajor); + if (this.Minor != otherMinor) + return this.Minor.CompareTo(otherMinor); + if (this.Patch != otherPatch) + return this.Patch.CompareTo(otherPatch); + if (this.Tag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Tag.Split('.', '-'); + string[] otherParts = otherTag.Split('.', '-'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + } +} -- cgit From 33760fa56bf2defd1b1e5a473cc45111b0b6f3f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:51:04 -0400 Subject: fix post-build step in toolkit project (#532) --- build/common.targets | 10 +++++----- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 2ee24e52..099d7cc9 100644 --- a/build/common.targets +++ b/build/common.targets @@ -95,7 +95,7 @@ - + @@ -106,15 +106,15 @@ - - - - + + + + diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index ffda899a..1028e657 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -17,6 +17,6 @@ - + -- cgit From 69b17f1db87d9aeb5dd6d6f9c81ac9ac62f2a6d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 02:06:28 -0400 Subject: move PathUtilities into toolkit (#532) --- src/SMAPI.Tests/Core/PathUtilitiesTests.cs | 70 ---------------------- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 8 ++- src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs | 70 ++++++++++++++++++++++ src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ContentCoordinator.cs | 3 +- src/SMAPI/Framework/ContentPack.cs | 2 +- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 2 +- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 10 ++-- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/Utilities/PathUtilities.cs | 65 -------------------- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Utilities/PathUtilities.cs | 65 ++++++++++++++++++++ 13 files changed, 152 insertions(+), 150 deletions(-) delete mode 100644 src/SMAPI.Tests/Core/PathUtilitiesTests.cs create mode 100644 src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs delete mode 100644 src/SMAPI/Framework/Utilities/PathUtilities.cs create mode 100644 src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs (limited to 'src') diff --git a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs b/src/SMAPI.Tests/Core/PathUtilitiesTests.cs deleted file mode 100644 index 268ba504..00000000 --- a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using NUnit.Framework; -using StardewModdingAPI.Framework.Utilities; - -namespace StardewModdingAPI.Tests.Core -{ - /// Unit tests for . - [TestFixture] - public class PathUtilitiesTests - { - /********* - ** Unit tests - *********/ - [Test(Description = "Assert that GetSegments returns the expected values.")] - [TestCase("", ExpectedResult = "")] - [TestCase("/", ExpectedResult = "")] - [TestCase("///", ExpectedResult = "")] - [TestCase("/usr/bin", ExpectedResult = "usr|bin")] - [TestCase("/usr//bin//", ExpectedResult = "usr|bin")] - [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "usr|bin|..|.|boop.exe")] - [TestCase(@"C:", ExpectedResult = "C:")] - [TestCase(@"C:/boop", ExpectedResult = "C:|boop")] - [TestCase(@"C:\boop\/usr//bin//.././boop.exe", ExpectedResult = "C:|boop|usr|bin|..|.|boop.exe")] - public string GetSegments(string path) - { - return string.Join("|", PathUtilities.GetSegments(path)); - } - - [Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] -#if SMAPI_FOR_WINDOWS - [TestCase("", ExpectedResult = "")] - [TestCase("/", ExpectedResult = "")] - [TestCase("///", ExpectedResult = "")] - [TestCase("/usr/bin", ExpectedResult = @"usr\bin")] - [TestCase("/usr//bin//", ExpectedResult = @"usr\bin")] - [TestCase("/usr//bin//.././boop.exe", ExpectedResult = @"usr\bin\..\.\boop.exe")] - [TestCase("C:", ExpectedResult = "C:")] - [TestCase("C:/boop", ExpectedResult = @"C:\boop")] - [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = @"C:\usr\bin\..\.\boop.exe")] -#else - [TestCase("", ExpectedResult = "")] - [TestCase("/", ExpectedResult = "/")] - [TestCase("///", ExpectedResult = "/")] - [TestCase("/usr/bin", ExpectedResult = "/usr/bin")] - [TestCase("/usr//bin//", ExpectedResult = "/usr/bin")] - [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "/usr/bin/.././boop.exe")] - [TestCase("C:", ExpectedResult = "C:")] - [TestCase("C:/boop", ExpectedResult = "C:/boop")] - [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] -#endif - public string NormalisePathSeparators(string path) - { - return PathUtilities.NormalisePathSeparators(path); - } - - [Test(Description = "Assert that GetRelativePath returns the expected values.")] -#if SMAPI_FOR_WINDOWS - [TestCase(@"C:\", @"C:\", ExpectedResult = "./")] - [TestCase(@"C:\grandparent\parent\child", @"C:\grandparent\parent\sibling", ExpectedResult = @"..\sibling")] - [TestCase(@"C:\grandparent\parent\child", @"C:\cousin\file.exe", ExpectedResult = @"..\..\..\cousin\file.exe")] -#else - [TestCase("/", "/", ExpectedResult = "./")] - [TestCase("/grandparent/parent/child", "/grandparent/parent/sibling", ExpectedResult = "../sibling")] - [TestCase("/grandparent/parent/child", "/cousin/file.exe", ExpectedResult = "../../../cousin/file.exe")] -#endif - public string GetRelativePath(string sourceDir, string targetPath) - { - return PathUtilities.GetRelativePath(sourceDir, targetPath); - } - } -} diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index f4d7b3e3..04c8d12f 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -56,7 +56,7 @@ Properties\GlobalAssemblyInfo.cs - + @@ -73,6 +73,10 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + diff --git a/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs new file mode 100644 index 00000000..229b9a14 --- /dev/null +++ b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Tests.Toolkit +{ + /// Unit tests for . + [TestFixture] + public class PathUtilitiesTests + { + /********* + ** Unit tests + *********/ + [Test(Description = "Assert that GetSegments returns the expected values.")] + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "")] + [TestCase("///", ExpectedResult = "")] + [TestCase("/usr/bin", ExpectedResult = "usr|bin")] + [TestCase("/usr//bin//", ExpectedResult = "usr|bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "usr|bin|..|.|boop.exe")] + [TestCase(@"C:", ExpectedResult = "C:")] + [TestCase(@"C:/boop", ExpectedResult = "C:|boop")] + [TestCase(@"C:\boop\/usr//bin//.././boop.exe", ExpectedResult = "C:|boop|usr|bin|..|.|boop.exe")] + public string GetSegments(string path) + { + return string.Join("|", PathUtilities.GetSegments(path)); + } + + [Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] +#if SMAPI_FOR_WINDOWS + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "")] + [TestCase("///", ExpectedResult = "")] + [TestCase("/usr/bin", ExpectedResult = @"usr\bin")] + [TestCase("/usr//bin//", ExpectedResult = @"usr\bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = @"usr\bin\..\.\boop.exe")] + [TestCase("C:", ExpectedResult = "C:")] + [TestCase("C:/boop", ExpectedResult = @"C:\boop")] + [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = @"C:\usr\bin\..\.\boop.exe")] +#else + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "/")] + [TestCase("///", ExpectedResult = "/")] + [TestCase("/usr/bin", ExpectedResult = "/usr/bin")] + [TestCase("/usr//bin//", ExpectedResult = "/usr/bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "/usr/bin/.././boop.exe")] + [TestCase("C:", ExpectedResult = "C:")] + [TestCase("C:/boop", ExpectedResult = "C:/boop")] + [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] +#endif + public string NormalisePathSeparators(string path) + { + return PathUtilities.NormalisePathSeparators(path); + } + + [Test(Description = "Assert that GetRelativePath returns the expected values.")] +#if SMAPI_FOR_WINDOWS + [TestCase(@"C:\", @"C:\", ExpectedResult = "./")] + [TestCase(@"C:\grandparent\parent\child", @"C:\grandparent\parent\sibling", ExpectedResult = @"..\sibling")] + [TestCase(@"C:\grandparent\parent\child", @"C:\cousin\file.exe", ExpectedResult = @"..\..\..\cousin\file.exe")] +#else + [TestCase("/", "/", ExpectedResult = "./")] + [TestCase("/grandparent/parent/child", "/grandparent/parent/sibling", ExpectedResult = "../sibling")] + [TestCase("/grandparent/parent/child", "/cousin/file.exe", ExpectedResult = "../../../cousin/file.exe")] +#endif + public string GetRelativePath(string sourceDir, string targetPath) + { + return PathUtilities.GetRelativePath(sourceDir, targetPath); + } + } +} diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index c2818cdd..a5dfac9d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -4,8 +4,8 @@ using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; namespace StardewModdingAPI.Framework.Content diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index c2614001..1a57dd22 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -9,10 +9,9 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; -using xTile; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 071fb872..ee6df1ec 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -3,7 +3,7 @@ using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; using xTile; namespace StardewModdingAPI.Framework diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index ce26c980..671dc21e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -9,7 +9,7 @@ using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using xTile; using xTile.Format; diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 26fe7198..61d2075c 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -157,13 +157,13 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate - if(string.IsNullOrWhiteSpace(directoryPath)) + if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException(nameof(directoryPath)); - if(string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - if(string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - if(!Directory.Exists(directoryPath)) + if (!Directory.Exists(directoryPath)) throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b5339183..d46caa55 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -7,7 +7,7 @@ using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs deleted file mode 100644 index 51d45ebd..00000000 --- a/src/SMAPI/Framework/Utilities/PathUtilities.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Diagnostics.Contracts; -using System.IO; -using System.Linq; - -namespace StardewModdingAPI.Framework.Utilities -{ - /// Provides utilities for normalising file paths. - internal static class PathUtilities - { - /********* - ** Properties - *********/ - /// The possible directory separator characters in a file path. - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - - /// The preferred directory separator chaeacter in an asset key. - private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); - - - /********* - ** Public methods - *********/ - /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). - /// The path to split. - /// The number of segments to match. Any additional segments will be merged into the last returned part. - public static string[] GetSegments(string path, int? limit = null) - { - return limit.HasValue - ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) - : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - } - - /// Normalise path separators in a file path. - /// The file path to normalise. - [Pure] - public static string NormalisePathSeparators(string path) - { - string[] parts = PathUtilities.GetSegments(path); - string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); - if (path.StartsWith(PathUtilities.PreferredPathSeparator)) - normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash - return normalised; - } - - /// Get a directory or file path relative to a given source path. - /// The source folder path. - /// The target folder or file path. - [Pure] - public static string GetRelativePath(string sourceDir, string targetPath) - { - // convert to URIs - Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); - - // get relative path - string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); - if (relative == "") - relative = "./"; - return relative; - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index aeb9b9fb..a1cfcf0c 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -25,9 +25,9 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 926bcbbc..3e6fdb24 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -157,7 +157,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs new file mode 100644 index 00000000..2e74e7d9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides utilities for normalising file paths. + public static class PathUtilities + { + /********* + ** Properties + *********/ + /// The possible directory separator characters in a file path. + private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + + /// The preferred directory separator chaeacter in an asset key. + private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); + + + /********* + ** Public methods + *********/ + /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). + /// The path to split. + /// The number of segments to match. Any additional segments will be merged into the last returned part. + public static string[] GetSegments(string path, int? limit = null) + { + return limit.HasValue + ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) + : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + /// Normalise path separators in a file path. + /// The file path to normalise. + [Pure] + public static string NormalisePathSeparators(string path) + { + string[] parts = PathUtilities.GetSegments(path); + string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); + if (path.StartsWith(PathUtilities.PreferredPathSeparator)) + normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Get a directory or file path relative to a given source path. + /// The source folder path. + /// The target folder or file path. + [Pure] + public static string GetRelativePath(string sourceDir, string targetPath) + { + // convert to URIs + Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); + + // get relative path + string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + if (relative == "") + relative = "./"; + return relative; + } + } +} -- cgit From 9b87d338aeb432d4417985f4e76b72499ed2ecac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 02:08:30 -0400 Subject: use separate pre-release version number for toolkit (#532) --- src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs | 5 ++++- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 4 ---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 9b55dac6..22dcdd96 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -2,6 +2,9 @@ using System.Reflection; using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] +[assembly: AssemblyProduct("SMAPI Toolkit")] +[assembly: AssemblyVersion("0.1.0")] +[assembly: AssemblyFileVersion("0.1.0")] [assembly: InternalsVisibleTo("StardewModdingAPI")] [assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 1028e657..6859d825 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -9,10 +9,6 @@ ..\..\bin\$(Configuration)\SMAPI.Toolkit - - - - -- cgit From 03860cca868aceb6af67d9a6ad171c8ceb9be7eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 19:26:09 -0400 Subject: add wiki compatibility list parsing to toolkit (#532) --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 2 +- .../Clients/Wiki/WikiCompatibilityClient.cs | 142 +++++++++++++++++++++ .../Clients/Wiki/WikiCompatibilityEntry.cs | 24 ++++ .../Clients/Wiki/WikiCompatibilityStatus.cs | 24 ++++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 33 +++++ .../StardewModdingAPI.Toolkit.csproj | 2 + 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/ModToolkit.cs (limited to 'src') diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 202a8376..9210565a 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs new file mode 100644 index 00000000..be03a120 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// An HTTP client for fetching mod metadata from the wiki compatibility list. + public class WikiCompatibilityClient : IDisposable + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the wiki API. + /// The base URL for the wiki API. + public WikiCompatibilityClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Fetch mod compatibility entries. + public async Task FetchAsync() + { + // fetch HTML + ResponseModel response = await this.Client + .GetAsync("") + .WithArguments(new + { + action = "parse", + page = "Modding:SMAPI_compatibility", + format = "json" + }) + .As(); + string html = response.Parse.Text["*"]; + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // find mod entries + HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); + if (modNodes == null) + throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); + + // parse + return this.ParseEntries(modNodes).ToArray(); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Parse valid mod compatibility entries. + /// The HTML compatibility entries. + private IEnumerable ParseEntries(IEnumerable nodes) + { + foreach (HtmlNode node in nodes) + { + // parse status + WikiCompatibilityStatus status; + { + string rawStatus = node.GetAttributeValue("data-status", null); + if (rawStatus == null) + continue; // not a mod node? + if (!Enum.TryParse(rawStatus, true, out status)) + throw new InvalidOperationException($"Unknown status '{rawStatus}' when parsing compatibility list."); + } + + // parse unofficial version + ISemanticVersion unofficialVersion = null; + { + string rawUnofficialVersion = node.GetAttributeValue("data-unofficial-version", null); + SemanticVersion.TryParse(rawUnofficialVersion, out unofficialVersion); + } + + // parse other fields + int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); + int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); + string githubRepo = node.GetAttributeValue("data-github", null); + string customSourceUrl = node.GetAttributeValue("data-custom-source", null); + + // yield model + yield return new WikiCompatibilityEntry + { + Status = status, + NexusID = nexusID, + ChucklefishID = chucklefishID, + GitHubRepo = githubRepo, + CustomSourceUrl = customSourceUrl, + UnofficialVersion = unofficialVersion + }; + } + } + + /// Get a nullable integer attribute value. + /// The HTML node. + /// The attribute name. + private int? GetNullableIntAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + if (raw != null && int.TryParse(raw, out int value)) + return value; + return null; + } + + /// The response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseModel + { + /// The parse API results. + public ResponseParseModel Parse { get; set; } + } + + /// The inner response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseParseModel + { + /// The parsed text. + public IDictionary Text { get; set; } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs new file mode 100644 index 00000000..ceeb2de5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// An entry in the mod compatibility list. + public class WikiCompatibilityEntry + { + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The version of the latest unofficial update, if applicable. + public ISemanticVersion UnofficialVersion { get; set; } + + /// The compatibility status. + public WikiCompatibilityStatus Status { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs new file mode 100644 index 00000000..06252b2f --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// The compatibility status for a mod. + public enum WikiCompatibilityStatus + { + /// The mod is compatible. + Ok = 0, + + /// The mod is compatible if you use an optional official download. + Optional = 1, + + /// The mod isn't compatible, but the player can fix it or there's a good alternative. + Workaround = 2, + + /// The mod isn't compatible. + Broken = 3, + + /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. + Abandoned = 4, + + /// The mod is no longer needed and should be removed. + Obsolete = 5 + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs new file mode 100644 index 00000000..6136186e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Toolkit +{ + /// A convenience wrapper for the various tools. + public class ModToolkit + { + /********* + ** Properties + *********/ + /// The default HTTP user agent for the toolkit. + private readonly string UserAgent; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModToolkit() + { + ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version); + this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; + } + + /// Extract mod metadata from the wiki compatibility list. + public async Task GetWikiCompatibilityListAsync() + { + var client = new WikiCompatibilityClient(this.UserAgent); + return await client.FetchAsync(); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 6859d825..6688a2a1 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -10,7 +10,9 @@ + + -- cgit From 738c0ce386590f55c185297065aeda62b1f5d06f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 May 2018 15:21:07 -0400 Subject: improve wiki parsing (#532) --- .../Clients/Wiki/WikiCompatibilityClient.cs | 23 +++++++++++++++++++--- .../Clients/Wiki/WikiCompatibilityEntry.cs | 9 +++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index be03a120..a68a0b4e 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -91,19 +91,25 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } // parse other fields + string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim(); + string[] ids = this.GetAttribute(node, "data-id")?.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); - string githubRepo = node.GetAttributeValue("data-github", null); - string customSourceUrl = node.GetAttributeValue("data-custom-source", null); + string githubRepo = this.GetAttribute(node, "data-github"); + string customSourceUrl = this.GetAttribute(node, "data-custom-source"); + string customUrl = this.GetAttribute(node, "data-custom-url"); // yield model yield return new WikiCompatibilityEntry { + ID = ids, + Name = name, Status = status, NexusID = nexusID, ChucklefishID = chucklefishID, GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, + CustomUrl = customUrl, UnofficialVersion = unofficialVersion }; } @@ -114,12 +120,23 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The attribute name. private int? GetNullableIntAttribute(HtmlNode node, string attributeName) { - string raw = node.GetAttributeValue(attributeName, null); + string raw = this.GetAttribute(node, attributeName); if (raw != null && int.TryParse(raw, out int value)) return value; return null; } + /// Get a strings attribute value. + /// The HTML node. + /// The attribute name. + private string GetAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + if (raw != null) + raw = HtmlEntity.DeEntitize(raw); + return raw; + } + /// The response model for the MediaWiki parse API. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs index ceeb2de5..9aabdcba 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -3,6 +3,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// An entry in the mod compatibility list. public class WikiCompatibilityEntry { + /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + public string[] ID { get; set; } + + /// The mod's display name. + public string Name { get; set; } + /// The mod ID on Nexus. public int? NexusID { get; set; } @@ -15,6 +21,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The URL to a non-GitHub source repo. public string CustomSourceUrl { get; set; } + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + /// The version of the latest unofficial update, if applicable. public ISemanticVersion UnofficialVersion { get; set; } -- cgit From f271939dfe1176a53ca36934f57d48bb0b9748c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 30 May 2018 21:16:15 -0400 Subject: tweak mod issues text --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index aeb9b9fb..844dc5d8 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -936,7 +936,7 @@ namespace StardewModdingAPI { string[] warnings = this.GetWarningText(metadata.Warnings).ToArray(); if (warnings.Length == 1) - this.Monitor.Log($" {metadata.DisplayName}: {warnings[0]}", LogLevel.Warn); + this.Monitor.Log($" {metadata.DisplayName} {warnings[0]}", LogLevel.Warn); else { this.Monitor.Log($" {metadata.DisplayName}:", LogLevel.Warn); -- cgit From 7dd7920503170b1cad4bf26ce93b18b17c446c19 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 30 May 2018 21:16:29 -0400 Subject: update compatibility list --- src/SMAPI/StardewModdingAPI.metadata.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 33568692..be9369f8 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -878,8 +878,8 @@ }, "Level Extender": { - "ID": "Devin Lematty.Level Extender", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "ID": "DevinLematty.LevelExtender", + "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3 "Default | UpdateKey": "Nexus:1471" }, @@ -1090,7 +1090,8 @@ }, "NPC Map Locations": { - "ID": "NPCMapLocationsMod", + "ID": "Bouhm.NPCMapLocations", + "FormerIDs": "NPCMapLocationsMod", // changed in 2.0 "Default | UpdateKey": "Nexus:239" }, @@ -1403,7 +1404,7 @@ }, "Siv's Marriage Mod": { - "ID": "6266959802", + "FormerIDs": "6266959802 | Siv.MarriageMod | medoli900.Siv's Marriage Mod", // changed in 1.2.3-unofficial versions "MapLocalVersions": { "0.0": "1.4" }, "Default | UpdateKey": "Nexus:366" }, @@ -1484,7 +1485,7 @@ "Sprint and Dash": { "ID": "SPDSprintAndDash", - "Default | UpdateKey": "Chucklefish:3531", + "Default | UpdateKey": "Nexus:235", "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 }, -- cgit From fa36e80a118b4bcaa021b349bff3e70e3b903976 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 30 May 2018 21:16:46 -0400 Subject: fix game content managers not cloning assets from IAssetLoader --- src/SMAPI/Framework/ContentCoordinator.cs | 23 +---------------- .../ContentManagers/BaseContentManager.cs | 29 ++++++++++++++++++++++ .../ContentManagers/GameContentManager.cs | 2 +- .../Framework/ContentManagers/IContentManager.cs | 5 ++++ 4 files changed, 36 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index c2614001..caa5b538 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; @@ -157,27 +156,7 @@ namespace StardewModdingAPI.Framework // get cloned asset T data = contentManager.Load(internalKey, language); - switch (data as object) - { - case Texture2D source: - { - int[] pixels = new int[source.Width * source.Height]; - source.GetData(pixels); - - Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); - clone.SetData(pixels); - return (T)(object)clone; - } - - case Dictionary source: - return (T)(object)new Dictionary(source); - - case Dictionary source: - return (T)(object)new Dictionary(source); - - default: - return data; - } + return contentManager.CloneIfPossible(data); } /// Purge assets from the cache that match one of the interceptors. diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index ff0e2de4..18aae05b 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; @@ -109,6 +110,34 @@ namespace StardewModdingAPI.Framework.ContentManagers } + /// Get a copy of the given asset if supported. + /// The asset type. + /// The asset to clone. + public T CloneIfPossible(T asset) + { + switch (asset as object) + { + case Texture2D source: + { + int[] pixels = new int[source.Width * source.Height]; + source.GetData(pixels); + + Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); + clone.SetData(pixels); + return (T)(object)clone; + } + + case Dictionary source: + return (T)(object)new Dictionary(source); + + case Dictionary source: + return (T)(object)new Dictionary(source); + + default: + return asset; + } + } + /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index cfedb5af..a53840bc 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -161,7 +161,7 @@ namespace StardewModdingAPI.Framework.ContentManagers T data; try { - data = loader.Load(info); + data = this.CloneIfPossible(loader.Load(info)); this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); } catch (Exception ex) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index aa5be9b6..1eb8b0ac 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -47,6 +47,11 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset value. void Inject(string assetName, T value); + /// Get a copy of the given asset if supported. + /// The asset type. + /// The asset to clone. + T CloneIfPossible(T asset); + /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] -- cgit From 9945408aa4ea2f6e07c2820f6aa1c160c68423d5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 30 May 2018 21:21:33 -0400 Subject: add summary and 'unofficial' status to wiki client (#532) --- .../Framework/Clients/Wiki/WikiCompatibilityClient.cs | 6 ++++-- .../Framework/Clients/Wiki/WikiCompatibilityEntry.cs | 3 +++ .../Framework/Clients/Wiki/WikiCompatibilityStatus.cs | 11 +++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index a68a0b4e..d0da42df 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -92,7 +92,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki // parse other fields string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim(); - string[] ids = this.GetAttribute(node, "data-id")?.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; + string summary = node.Descendants("td").FirstOrDefault(p => p.GetAttributeValue("class", null) == "summary")?.InnerText.Trim(); + string[] ids = this.GetAttribute(node, "data-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); string githubRepo = this.GetAttribute(node, "data-github"); @@ -110,7 +111,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, CustomUrl = customUrl, - UnofficialVersion = unofficialVersion + UnofficialVersion = unofficialVersion, + Summary = summary }; } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs index 9aabdcba..8bc66e20 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -29,5 +29,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The compatibility status. public WikiCompatibilityStatus Status { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + public string Summary { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs index 06252b2f..a1d2dfae 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -9,16 +9,19 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The mod is compatible if you use an optional official download. Optional = 1, + /// The mod is compatible if you use an unofficial update. + Unofficial = 2, + /// The mod isn't compatible, but the player can fix it or there's a good alternative. - Workaround = 2, + Workaround = 3, /// The mod isn't compatible. - Broken = 3, + Broken = 4, /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. - Abandoned = 4, + Abandoned = 5, /// The mod is no longer needed and should be removed. - Obsolete = 5 + Obsolete = 6 } } -- cgit From e5f8b1419afa2ad4bece4fde2286b967476c1031 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 31 May 2018 22:31:19 -0400 Subject: fix Nexus mod update alerts not showing HTTPs links --- docs/release-notes.md | 1 + src/SMAPI.Web/appsettings.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2a7835c0..b2a5b22a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ * Fixed `world_setseason` command not running season-change logic. * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) + * Fixed Nexus mod update alerts not showing HTTPS links. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 095707a8..53da6307 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -31,7 +31,7 @@ "GitHubPassword": null, // see top note "NexusUserAgent": "Nexus Client v0.63.15", - "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", + "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley", "NexusModUrlFormat": "mods/{0}", "PastebinBaseUrl": "https://pastebin.com/", -- cgit From 558fb8a865b638cf5536856e7dcab44823feeaf3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 31 May 2018 22:47:56 -0400 Subject: move location events into new event system (#310) --- .../Events/EventArgsLocationObjectsChanged.cs | 1 - src/SMAPI/Events/IModEvents.cs | 9 ++++ src/SMAPI/Events/IWorldEvents.cs | 20 ++++++++ src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs | 39 +++++++++++++++ src/SMAPI/Events/WorldLocationsChangedEventArgs.cs | 33 +++++++++++++ src/SMAPI/Events/WorldObjectsChangedEventArgs.cs | 40 ++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 26 ++++++++-- src/SMAPI/Framework/Events/ManagedEvent.cs | 20 +++++++- src/SMAPI/Framework/Events/ManagedEventBase.cs | 9 ++-- src/SMAPI/Framework/Events/ModEvents.cs | 26 ++++++++++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 56 ++++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 16 +++++-- src/SMAPI/Framework/SGame.cs | 3 ++ src/SMAPI/IModHelper.cs | 5 ++ src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 7 +++ 16 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 src/SMAPI/Events/IModEvents.cs create mode 100644 src/SMAPI/Events/IWorldEvents.cs create mode 100644 src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldLocationsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldObjectsChangedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModWorldEvents.cs (limited to 'src') diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index 410ef6e6..3bb387d5 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; -using Netcode; using StardewValley; using SObject = StardewValley.Object; diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs new file mode 100644 index 00000000..99e5523f --- /dev/null +++ b/src/SMAPI/Events/IModEvents.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI.Events +{ + /// Manages access to events raised by SMAPI. + public interface IModEvents + { + /// Events raised when something changes in the world. + IWorldEvents World { get; } + } +} diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs new file mode 100644 index 00000000..5c713250 --- /dev/null +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -0,0 +1,20 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Provides events raised when something changes in the world. + public interface IWorldEvents + { + /********* + ** Events + *********/ + /// Raised after a game location is added or removed. + event EventHandler LocationsChanged; + + /// Raised after buildings are added or removed in a location. + event EventHandler BuildingsChanged; + + /// Raised after objects are added or removed in a location. + event EventHandler ObjectsChanged; + } +} diff --git a/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs new file mode 100644 index 00000000..1f68fd02 --- /dev/null +++ b/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldBuildingsChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public WorldBuildingsChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs new file mode 100644 index 00000000..5cf77959 --- /dev/null +++ b/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldLocationsChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public WorldLocationsChangedEventArgs(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs new file mode 100644 index 00000000..fb20acd4 --- /dev/null +++ b/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldObjectsChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + public WorldObjectsChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 84036127..53ea699a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; @@ -10,7 +9,23 @@ namespace StardewModdingAPI.Framework.Events internal class EventManager { /********* - ** Properties + ** Events (new) + *********/ + /**** + ** World + ****/ + /// Raised after a game location is added or removed. + public readonly ManagedEvent World_LocationsChanged; + + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent World_BuildingsChanged; + + /// Raised after objects are added or removed in a location. + public readonly ManagedEvent World_ObjectsChanged; + + + /********* + ** Events (old) *********/ /**** ** ContentEvents @@ -209,7 +224,12 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); - // init events + // init events (new) + this.World_BuildingsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationsChanged)); + this.World_LocationsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingsChanged)); + this.World_ObjectsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectsChanged)); + + // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); this.Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index e54a4fd3..c1ebf6c7 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -27,9 +27,17 @@ namespace StardewModdingAPI.Framework.Events /// Add an event handler. /// The event handler. public void Add(EventHandler handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// Add an event handler. + /// The event handler. + /// The mod which added the event handler. + public void Add(EventHandler handler, IModMetadata mod) { this.Event += handler; - this.AddTracking(handler, this.Event?.GetInvocationList().Cast>()); + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); } /// Remove an event handler. @@ -84,9 +92,17 @@ namespace StardewModdingAPI.Framework.Events /// Add an event handler. /// The event handler. public void Add(EventHandler handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// Add an event handler. + /// The event handler. + /// The mod which added the event handler. + public void Add(EventHandler handler, IModMetadata mod) { this.Event += handler; - this.AddTracking(handler, this.Event?.GetInvocationList().Cast()); + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast()); } /// Remove an event handler. diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs index 7e42d613..f3a278dc 100644 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI.Framework.Events private readonly IMonitor Monitor; /// The mod registry with which to identify mods. - private readonly ModRegistry ModRegistry; + protected readonly ModRegistry ModRegistry; /// The display names for the mods which added each delegate. private readonly IDictionary SourceMods = new Dictionary(); @@ -50,11 +50,12 @@ namespace StardewModdingAPI.Framework.Events } /// Track an event handler. + /// The mod which added the handler. /// The event handler. /// The updated event invocation list. - protected void AddTracking(TEventHandler handler, IEnumerable invocationList) + protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable invocationList) { - this.SourceMods[handler] = this.ModRegistry.GetFromStack(); + this.SourceMods[handler] = mod; this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; } @@ -64,7 +65,7 @@ namespace StardewModdingAPI.Framework.Events protected void RemoveTracking(TEventHandler handler, IEnumerable invocationList) { this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; - if(!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) + if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs new file mode 100644 index 00000000..cc4cf8d7 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -0,0 +1,26 @@ +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Manages access to events raised by SMAPI. + internal class ModEvents : IModEvents + { + /********* + ** Accessors + *********/ + /// Events raised when something changes in the world. + public IWorldEvents World { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + public ModEvents(IModMetadata mod, EventManager eventManager) + { + this.World = new ModWorldEvents(mod, eventManager); + } + } +} diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs new file mode 100644 index 00000000..a76a7eb5 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -0,0 +1,56 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when something changes in the world. + public class ModWorldEvents : IWorldEvents + { + /********* + ** Properties + *********/ + /// The underlying event manager. + private readonly EventManager EventManager; + + /// The mod which uses this instance. + private readonly IModMetadata Mod; + + + /********* + ** Accessors + *********/ + /// Raised after a game location is added or removed. + public event EventHandler LocationsChanged + { + add => this.EventManager.World_LocationsChanged.Add(value, this.Mod); + remove => this.EventManager.World_LocationsChanged.Remove(value); + } + + /// Raised after buildings are added or removed in a location. + public event EventHandler BuildingsChanged + { + add => this.EventManager.World_BuildingsChanged.Add(value, this.Mod); + remove => this.EventManager.World_BuildingsChanged.Remove(value); + } + + /// Raised after objects are added or removed in a location. + public event EventHandler ObjectsChanged + { + add => this.EventManager.World_ObjectsChanged.Add(value); + remove => this.EventManager.World_ObjectsChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModWorldEvents(IModMetadata mod, EventManager eventManager) + { + this.Mod = mod; + this.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 26fe7198..92cb9d94 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; @@ -33,6 +34,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The full path to the mod's folder. public string DirectoryPath { get; } + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + public IModEvents Events { get; } + /// An API for loading content assets. public IContentHelper Content { get; } @@ -59,6 +63,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod's unique ID. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. + /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. /// an API for fetching metadata about loaded mods. @@ -70,7 +75,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -91,6 +96,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ContentPacks = contentPacks.ToArray(); this.CreateContentPack = createContentPack; this.DeprecationManager = deprecationManager; + this.Events = events; } /**** @@ -157,13 +163,13 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate - if(string.IsNullOrWhiteSpace(directoryPath)) + if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException(nameof(directoryPath)); - if(string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - if(string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - if(!Directory.Exists(directoryPath)) + if (!Directory.Exists(directoryPath)) throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 369f1f40..e7e9f74f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -543,6 +543,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } + this.Events.World_LocationsChanged.Raise(new WorldLocationsChangedEventArgs(added, removed)); this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } @@ -559,6 +560,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); + this.Events.World_ObjectsChanged.Raise(new WorldObjectsChangedEventArgs(location, added, removed)); this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } @@ -570,6 +572,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); + this.Events.World_BuildingsChanged.Raise(new WorldBuildingsChangedEventArgs(location, added, removed)); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } } diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 5e39161d..68c2f1c4 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using StardewModdingAPI.Events; namespace StardewModdingAPI { @@ -12,6 +13,10 @@ namespace StardewModdingAPI /// The full path to the mod's folder. string DirectoryPath { get; } + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + [Obsolete("This is an experimental interface which may change at any time. Don't depend on this for released mods.")] + IModEvents Events { get; } + /// An API for loading content assets. IContentHelper Content { get; } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 844dc5d8..48ad922b 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -840,6 +840,7 @@ namespace StardewModdingAPI IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IModHelper modHelper; { + IModEvents events = new ModEvents(metadata, this.EventManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); @@ -854,7 +855,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e9e0ea54..6a062930 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -86,17 +86,23 @@ Properties\GlobalAssemblyInfo.cs + + + + + + @@ -129,6 +135,7 @@ + -- cgit From cca7bf197079975bf310ca90c03b78d0f77fdf02 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 01:15:26 -0400 Subject: rename new events for clarity (#310) --- src/SMAPI/Events/IWorldEvents.cs | 6 ++-- .../Events/WorldBuildingListChangedEventArgs.cs | 39 +++++++++++++++++++++ src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs | 39 --------------------- .../Events/WorldLocationListChangedEventArgs.cs | 33 ++++++++++++++++++ src/SMAPI/Events/WorldLocationsChangedEventArgs.cs | 33 ------------------ .../Events/WorldObjectListChangedEventArgs.cs | 40 ++++++++++++++++++++++ src/SMAPI/Events/WorldObjectsChangedEventArgs.cs | 40 ---------------------- src/SMAPI/Framework/Events/EventManager.cs | 12 +++---- src/SMAPI/Framework/Events/ModWorldEvents.cs | 18 +++++----- src/SMAPI/Framework/SGame.cs | 6 ++-- src/SMAPI/StardewModdingAPI.csproj | 6 ++-- 11 files changed, 136 insertions(+), 136 deletions(-) create mode 100644 src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldLocationListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldLocationsChangedEventArgs.cs create mode 100644 src/SMAPI/Events/WorldObjectListChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/WorldObjectsChangedEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 5c713250..a9a5fe6b 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -9,12 +9,12 @@ namespace StardewModdingAPI.Events ** Events *********/ /// Raised after a game location is added or removed. - event EventHandler LocationsChanged; + event EventHandler LocationListChanged; /// Raised after buildings are added or removed in a location. - event EventHandler BuildingsChanged; + event EventHandler BuildingListChanged; /// Raised after objects are added or removed in a location. - event EventHandler ObjectsChanged; + event EventHandler ObjectListChanged; } } diff --git a/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs new file mode 100644 index 00000000..e73b9396 --- /dev/null +++ b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldBuildingListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public WorldBuildingListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs deleted file mode 100644 index 1f68fd02..00000000 --- a/src/SMAPI/Events/WorldBuildingsChangedEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; -using StardewValley.Buildings; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldBuildingsChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The buildings added to the location. - public IEnumerable Added { get; } - - /// The buildings removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The buildings added to the location. - /// The buildings removed from the location. - public WorldBuildingsChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs new file mode 100644 index 00000000..8bc26a43 --- /dev/null +++ b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldLocationListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public WorldLocationListChangedEventArgs(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs deleted file mode 100644 index 5cf77959..00000000 --- a/src/SMAPI/Events/WorldLocationsChangedEventArgs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldLocationsChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The added locations. - public IEnumerable Added { get; } - - /// The removed locations. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The added locations. - /// The removed locations. - public WorldLocationsChangedEventArgs(IEnumerable added, IEnumerable removed) - { - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs new file mode 100644 index 00000000..5623a49b --- /dev/null +++ b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldObjectListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + public WorldObjectListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs deleted file mode 100644 index fb20acd4..00000000 --- a/src/SMAPI/Events/WorldObjectsChangedEventArgs.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using StardewValley; -using Object = StardewValley.Object; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class WorldObjectsChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The objects added to the location. - public IEnumerable> Added { get; } - - /// The objects removed from the location. - public IEnumerable> Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The objects added to the location. - /// The objects removed from the location. - public WorldObjectsChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 53ea699a..0e1e6241 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -15,13 +15,13 @@ namespace StardewModdingAPI.Framework.Events ** World ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent World_LocationsChanged; + public readonly ManagedEvent World_LocationListChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent World_BuildingsChanged; + public readonly ManagedEvent World_BuildingListChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent World_ObjectsChanged; + public readonly ManagedEvent World_ObjectListChanged; /********* @@ -225,9 +225,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) - this.World_BuildingsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationsChanged)); - this.World_LocationsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingsChanged)); - this.World_ObjectsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectsChanged)); + this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index a76a7eb5..7036b765 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -20,24 +20,24 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after a game location is added or removed. - public event EventHandler LocationsChanged + public event EventHandler LocationListChanged { - add => this.EventManager.World_LocationsChanged.Add(value, this.Mod); - remove => this.EventManager.World_LocationsChanged.Remove(value); + add => this.EventManager.World_LocationListChanged.Add(value, this.Mod); + remove => this.EventManager.World_LocationListChanged.Remove(value); } /// Raised after buildings are added or removed in a location. - public event EventHandler BuildingsChanged + public event EventHandler BuildingListChanged { - add => this.EventManager.World_BuildingsChanged.Add(value, this.Mod); - remove => this.EventManager.World_BuildingsChanged.Remove(value); + add => this.EventManager.World_BuildingListChanged.Add(value, this.Mod); + remove => this.EventManager.World_BuildingListChanged.Remove(value); } /// Raised after objects are added or removed in a location. - public event EventHandler ObjectsChanged + public event EventHandler ObjectListChanged { - add => this.EventManager.World_ObjectsChanged.Add(value); - remove => this.EventManager.World_ObjectsChanged.Remove(value); + add => this.EventManager.World_ObjectListChanged.Add(value); + remove => this.EventManager.World_ObjectListChanged.Remove(value); } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index e7e9f74f..9442c749 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -543,7 +543,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } - this.Events.World_LocationsChanged.Raise(new WorldLocationsChangedEventArgs(added, removed)); + this.Events.World_LocationListChanged.Raise(new WorldLocationListChangedEventArgs(added, removed)); this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } @@ -560,7 +560,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); - this.Events.World_ObjectsChanged.Raise(new WorldObjectsChangedEventArgs(location, added, removed)); + this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } @@ -572,7 +572,7 @@ namespace StardewModdingAPI.Framework var removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); - this.Events.World_BuildingsChanged.Raise(new WorldBuildingsChangedEventArgs(location, added, removed)); + this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6a062930..951a9e6b 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -88,9 +88,9 @@ - - - + + + -- cgit From b3f116a8f123387b1f2f9ce1a099d1fd8081708d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 01:58:31 -0400 Subject: add terrain feature list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 3 ++ .../WorldTerrainFeatureListChangedEventArgs.cs | 40 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 22 +++++++++--- .../Framework/StateTracking/LocationTracker.cs | 12 +++++-- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index a9a5fe6b..7ec26bae 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -16,5 +16,8 @@ namespace StardewModdingAPI.Events /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; + + /// Raised after terrain features are added or removed in a location. + event EventHandler TerrainFeatureListChanged; } } diff --git a/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..cb089811 --- /dev/null +++ b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldTerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The terrain features added to the location. + public IEnumerable> Added { get; } + + /// The terrain features removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The terrain features added to the location. + /// The terrain features removed from the location. + public WorldTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 0e1e6241..c909c1bd 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after objects are added or removed in a location. public readonly ManagedEvent World_ObjectListChanged; + /// Raised after terrain features are added or removed in a location. + public readonly ManagedEvent World_TerrainFeatureListChanged; + /********* ** Events (old) @@ -228,6 +231,7 @@ namespace StardewModdingAPI.Framework.Events this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.World_TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 7036b765..14646c5d 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -40,6 +40,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_ObjectListChanged.Remove(value); } + /// Raised after terrain features are added or removed in a location. + public event EventHandler TerrainFeatureListChanged + { + add => this.EventManager.World_TerrainFeatureListChanged.Add(value); + remove => this.EventManager.World_TerrainFeatureListChanged.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 9442c749..02d6dd2c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -18,11 +18,14 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; using StardewValley.Locations; using StardewValley.Menus; +using StardewValley.TerrainFeatures; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; +using Object = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -556,8 +559,8 @@ namespace StardewModdingAPI.Framework if (watcher.ObjectsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.ObjectsWatcher.Added.ToArray(); - var removed = watcher.ObjectsWatcher.Removed.ToArray(); + KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); @@ -568,13 +571,24 @@ namespace StardewModdingAPI.Framework if (watcher.BuildingsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.BuildingsWatcher.Added.ToArray(); - var removed = watcher.BuildingsWatcher.Removed.ToArray(); + Building[] added = watcher.BuildingsWatcher.Added.ToArray(); + Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); watcher.BuildingsWatcher.Reset(); this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + + // terrain features changed + if (watcher.TerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); + watcher.TerrainFeaturesWatcher.Reset(); + + this.Events.World_TerrainFeatureListChanged.Raise(new WorldTerrainFeatureListChangedEventArgs(location, added, removed)); + } } } else diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 07570401..d31b1128 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -6,6 +6,7 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; +using StardewValley.TerrainFeatures; using Object = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -29,12 +30,15 @@ namespace StardewModdingAPI.Framework.StateTracking /// The tracked location. public GameLocation Location { get; } - /// Tracks changes to the location's buildings. + /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } - /// Tracks changes to the location's objects. + /// Tracks added or removed objects. public IDictionaryWatcher ObjectsWatcher { get; } + /// Tracks added or removed terrain features. + public IDictionaryWatcher TerrainFeaturesWatcher { get; } + /********* ** Public methods @@ -50,11 +54,13 @@ namespace StardewModdingAPI.Framework.StateTracking this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, - this.ObjectsWatcher + this.ObjectsWatcher, + this.TerrainFeaturesWatcher }); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 951a9e6b..50d8c533 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -88,6 +88,7 @@ + -- cgit From 07bbfea7dd9dac5bbacf8bfc3c35d3f65ec71b75 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 02:14:01 -0400 Subject: add NPC list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 3 ++ src/SMAPI/Events/WorldNpcListChangedEventArgs.cs | 38 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 35 +++++++++++++------- .../Framework/StateTracking/LocationTracker.cs | 7 +++- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 src/SMAPI/Events/WorldNpcListChangedEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 7ec26bae..ce288ae1 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Events /// Raised after buildings are added or removed in a location. event EventHandler BuildingListChanged; + /// Raised after NPCs are added or removed in a location. + event EventHandler NpcListChanged; + /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; diff --git a/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs new file mode 100644 index 00000000..e251f894 --- /dev/null +++ b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldNpcListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The NPCs added to the location. + public IEnumerable Added { get; } + + /// The NPCs removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The NPCs added to the location. + /// The NPCs removed from the location. + public WorldNpcListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index c909c1bd..cb331e7a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after buildings are added or removed in a location. public readonly ManagedEvent World_BuildingListChanged; + /// Raised after NPCs are added or removed in a location. + public readonly ManagedEvent World_NpcListChanged; + /// Raised after objects are added or removed in a location. public readonly ManagedEvent World_ObjectListChanged; @@ -230,6 +233,7 @@ namespace StardewModdingAPI.Framework.Events // init events (new) this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.World_TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 14646c5d..ca851550 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -33,6 +33,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_BuildingListChanged.Remove(value); } + /// Raised after NPCs are added or removed in a location. + public event EventHandler NpcListChanged + { + add => this.EventManager.World_NpcListChanged.Add(value); + remove => this.EventManager.World_NpcListChanged.Remove(value); + } + /// Raised after objects are added or removed in a location. public event EventHandler ObjectListChanged { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 02d6dd2c..38f96566 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -555,18 +555,6 @@ namespace StardewModdingAPI.Framework { foreach (LocationTracker watcher in this.LocationsWatcher.Locations) { - // objects changed - if (watcher.ObjectsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); - KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); - watcher.ObjectsWatcher.Reset(); - - this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); - this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); - } - // buildings changed if (watcher.BuildingsWatcher.IsChanged) { @@ -579,6 +567,29 @@ namespace StardewModdingAPI.Framework this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + // NPCs changed + if (watcher.NpcsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + NPC[] added = watcher.NpcsWatcher.Added.ToArray(); + NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); + watcher.NpcsWatcher.Reset(); + + this.Events.World_NpcListChanged.Raise(new WorldNpcListChangedEventArgs(location, added, removed)); + } + + // objects changed + if (watcher.ObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); + watcher.ObjectsWatcher.Reset(); + + this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); + this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + } + // terrain features changed if (watcher.TerrainFeaturesWatcher.IsChanged) { diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index d31b1128..4ac455ac 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.StateTracking /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } + /// Tracks added or removed NPCs. + public ICollectionWatcher NpcsWatcher { get; } + /// Tracks added or removed objects. public IDictionaryWatcher ObjectsWatcher { get; } @@ -50,15 +53,17 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, + this.NpcsWatcher, this.ObjectsWatcher, this.TerrainFeaturesWatcher }); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 50d8c533..37b624fb 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -88,6 +88,7 @@ + -- cgit From a2523696fd539621351c178f6ff57ade19dd3e34 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 21:40:17 -0400 Subject: fix issue where a mod crashing in CanEdit/CanLoad could cause an abort-retry loop --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 59 ++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b2a5b22a..e93c6bff 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -38,6 +38,7 @@ * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. * Fixed mods able to intercept other mods' assets via the internal asset keys. * Fixed mods able to indirectly change other mods' data through shared content caches. + * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten (see above). diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index caa5b538..15d39a5c 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -11,7 +11,6 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; using StardewValley; -using xTile; namespace StardewModdingAPI.Framework { @@ -179,12 +178,36 @@ namespace StardewModdingAPI.Framework { // check loaders MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); - if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) - return true; + foreach (IAssetLoader loader in loaders) + { + try + { + if ((bool)canLoadGeneric.Invoke(loader, new object[] { asset })) + return true; + } + catch (Exception ex) + { + this.GetModFor(loader).LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } // check editors MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); - return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); + foreach (IAssetEditor editor in editors) + { + try + { + if ((bool)canEditGeneric.Invoke(editor, new object[] { asset })) + return true; + } + catch (Exception ex) + { + this.GetModFor(editor).LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + // asset not affected by a loader or editor + return false; }); } @@ -259,5 +282,33 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Remove(contentManager); } + + /// Get the mod which registered an asset loader. + /// The asset loader. + /// The given loader couldn't be matched to a mod. + private IModMetadata GetModFor(IAssetLoader loader) + { + foreach (var pair in this.Loaders) + { + if (pair.Value.Contains(loader)) + return pair.Key; + } + + throw new KeyNotFoundException("This loader isn't associated with a known mod."); + } + + /// Get the mod which registered an asset editor. + /// The asset editor. + /// The given editor couldn't be matched to a mod. + private IModMetadata GetModFor(IAssetEditor editor) + { + foreach (var pair in this.Editors) + { + if (pair.Value.Contains(editor)) + return pair.Key; + } + + throw new KeyNotFoundException("This editor isn't associated with a known mod."); + } } } -- cgit From 92006bd6ed20365f1ded4bd07387b0ba9ed0ff92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Jun 2018 23:16:42 -0400 Subject: add large terrain feature list changed event (#310) --- src/SMAPI/Events/IWorldEvents.cs | 5 ++- ...WorldLargeTerrainFeatureListChangedEventArgs.cs | 39 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 6 +++- src/SMAPI/Framework/Events/ModWorldEvents.cs | 9 ++++- src/SMAPI/Framework/SGame.cs | 11 ++++++ .../Framework/StateTracking/LocationTracker.cs | 5 +++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index ce288ae1..6a43baf2 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -14,13 +14,16 @@ namespace StardewModdingAPI.Events /// Raised after buildings are added or removed in a location. event EventHandler BuildingListChanged; + /// Raised after large terrain features (like bushes) are added or removed in a location. + event EventHandler LargeTerrainFeatureListChanged; + /// Raised after NPCs are added or removed in a location. event EventHandler NpcListChanged; /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; - /// Raised after terrain features are added or removed in a location. + /// Raised after terrain features (like floors and trees) are added or removed in a location. event EventHandler TerrainFeatureListChanged; } } diff --git a/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..053a0e41 --- /dev/null +++ b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class WorldLargeTerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The large terrain features added to the location. + public IEnumerable Added { get; } + + /// The large terrain features removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The large terrain features added to the location. + /// The large terrain features removed from the location. + public WorldLargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index cb331e7a..7ce8c640 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -20,13 +20,16 @@ namespace StardewModdingAPI.Framework.Events /// Raised after buildings are added or removed in a location. public readonly ManagedEvent World_BuildingListChanged; + /// Raised after large terrain features (like bushes) are added or removed in a location. + public readonly ManagedEvent World_LargeTerrainFeatureListChanged; + /// Raised after NPCs are added or removed in a location. public readonly ManagedEvent World_NpcListChanged; /// Raised after objects are added or removed in a location. public readonly ManagedEvent World_ObjectListChanged; - /// Raised after terrain features are added or removed in a location. + /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; @@ -232,6 +235,7 @@ namespace StardewModdingAPI.Framework.Events // init events (new) this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.World_NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.World_ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index ca851550..db03e447 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -33,6 +33,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_BuildingListChanged.Remove(value); } + /// Raised after large terrain features (like bushes) are added or removed in a location. + public event EventHandler LargeTerrainFeatureListChanged + { + add => this.EventManager.World_LargeTerrainFeatureListChanged.Add(value, this.Mod); + remove => this.EventManager.World_LargeTerrainFeatureListChanged.Remove(value); + } + /// Raised after NPCs are added or removed in a location. public event EventHandler NpcListChanged { @@ -47,7 +54,7 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.World_ObjectListChanged.Remove(value); } - /// Raised after terrain features are added or removed in a location. + /// Raised after terrain features (like floors and trees) are added or removed in a location. public event EventHandler TerrainFeatureListChanged { add => this.EventManager.World_TerrainFeatureListChanged.Add(value); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 38f96566..90dbacfe 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -567,6 +567,17 @@ namespace StardewModdingAPI.Framework this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } + // large terrain features changed + if (watcher.LargeTerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); + LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); + watcher.LargeTerrainFeaturesWatcher.Reset(); + + this.Events.World_LargeTerrainFeatureListChanged.Raise(new WorldLargeTerrainFeatureListChangedEventArgs(location, added, removed)); + } + // NPCs changed if (watcher.NpcsWatcher.IsChanged) { diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 4ac455ac..1b4c0b19 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.StateTracking /// Tracks added or removed buildings. public ICollectionWatcher BuildingsWatcher { get; } + /// Tracks added or removed large terrain features. + public ICollectionWatcher LargeTerrainFeaturesWatcher { get; } + /// Tracks added or removed NPCs. public ICollectionWatcher NpcsWatcher { get; } @@ -56,6 +59,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures); this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); @@ -63,6 +67,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, + this.LargeTerrainFeaturesWatcher, this.NpcsWatcher, this.ObjectsWatcher, this.TerrainFeaturesWatcher diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 37b624fb..f9c93671 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -89,6 +89,7 @@ + -- cgit From a29e2c59d4a01d5b8828644efd6becabc59af28e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 01:04:02 -0400 Subject: disambiguate legacy events internally (#310) --- src/SMAPI/Events/IWorldEvents.cs | 5 +---- src/SMAPI/Events/LocationEvents.cs | 12 ++++++------ src/SMAPI/Framework/Events/EventManager.cs | 12 ++++++------ src/SMAPI/Framework/SGame.cs | 6 +++--- 4 files changed, 16 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 6a43baf2..067a79bc 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -2,12 +2,9 @@ using System; namespace StardewModdingAPI.Events { - /// Provides events raised when something changes in the world. + /// Events raised when something changes in the world. public interface IWorldEvents { - /********* - ** Events - *********/ /// Raised after a game location is added or removed. event EventHandler LocationListChanged; diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index ff75c619..e2108de0 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -19,22 +19,22 @@ namespace StardewModdingAPI.Events /// Raised after a game location is added or removed. public static event EventHandler LocationsChanged { - add => LocationEvents.EventManager.Location_LocationsChanged.Add(value); - remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value); + add => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Add(value); + remove => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Remove(value); } /// Raised after buildings are added or removed in a location. public static event EventHandler BuildingsChanged { - add => LocationEvents.EventManager.Location_BuildingsChanged.Add(value); - remove => LocationEvents.EventManager.Location_BuildingsChanged.Remove(value); + add => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Add(value); + remove => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Remove(value); } /// Raised after objects are added or removed in a location. public static event EventHandler ObjectsChanged { - add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value); - remove => LocationEvents.EventManager.Location_ObjectsChanged.Remove(value); + add => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Add(value); + remove => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Remove(value); } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 7ce8c640..100e4e43 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -133,13 +133,13 @@ namespace StardewModdingAPI.Framework.Events ** LocationEvents ****/ /// Raised after a game location is added or removed. - public readonly ManagedEvent Location_LocationsChanged; + public readonly ManagedEvent Legacy_Location_LocationsChanged; /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent Location_BuildingsChanged; + public readonly ManagedEvent Legacy_Location_BuildingsChanged; /// Raised after objects are added or removed in a location. - public readonly ManagedEvent Location_ObjectsChanged; + public readonly ManagedEvent Legacy_Location_ObjectsChanged; /**** ** MenuEvents @@ -273,9 +273,9 @@ namespace StardewModdingAPI.Framework.Events this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); - this.Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); + this.Legacy_Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Legacy_Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); + this.Legacy_Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 90dbacfe..1611861f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -547,7 +547,7 @@ namespace StardewModdingAPI.Framework } this.Events.World_LocationListChanged.Raise(new WorldLocationListChangedEventArgs(added, removed)); - this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); + this.Events.Legacy_Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } // raise location contents changed @@ -564,7 +564,7 @@ namespace StardewModdingAPI.Framework watcher.BuildingsWatcher.Reset(); this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); - this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); + this.Events.Legacy_Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); } // large terrain features changed @@ -598,7 +598,7 @@ namespace StardewModdingAPI.Framework watcher.ObjectsWatcher.Reset(); this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); - this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + this.Events.Legacy_Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } // terrain features changed -- cgit From 97a2bdfdd443b3d5b79f48eb1d718ebf255f5e0f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 01:47:33 -0400 Subject: add base implementation for mod event classes (#310) --- src/SMAPI/Framework/Events/ModEventsBase.cs | 28 ++++++++++++++++++++++++++++ src/SMAPI/Framework/Events/ModWorldEvents.cs | 17 ++--------------- src/SMAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Framework/Events/ModEventsBase.cs (limited to 'src') diff --git a/src/SMAPI/Framework/Events/ModEventsBase.cs b/src/SMAPI/Framework/Events/ModEventsBase.cs new file mode 100644 index 00000000..545c58a8 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModEventsBase.cs @@ -0,0 +1,28 @@ +namespace StardewModdingAPI.Framework.Events +{ + /// An internal base class for event API classes. + internal abstract class ModEventsBase + { + /********* + ** Properties + *********/ + /// The underlying event manager. + protected readonly EventManager EventManager; + + /// The mod which uses this instance. + protected readonly IModMetadata Mod; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModEventsBase(IModMetadata mod, EventManager eventManager) + { + this.Mod = mod; + this.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index db03e447..e1a53e0c 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -4,18 +4,8 @@ using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework.Events { /// Events raised when something changes in the world. - public class ModWorldEvents : IWorldEvents + internal class ModWorldEvents : ModEventsBase, IWorldEvents { - /********* - ** Properties - *********/ - /// The underlying event manager. - private readonly EventManager EventManager; - - /// The mod which uses this instance. - private readonly IModMetadata Mod; - - /********* ** Accessors *********/ @@ -69,9 +59,6 @@ namespace StardewModdingAPI.Framework.Events /// The mod which uses this instance. /// The underlying event manager. internal ModWorldEvents(IModMetadata mod, EventManager eventManager) - { - this.Mod = mod; - this.EventManager = eventManager; - } + : base(mod, eventManager) { } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f9c93671..7b9629e2 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -98,6 +98,7 @@ + -- cgit From 0df7a967a6980db7f4da8d393feae97c968e3375 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 01:48:35 -0400 Subject: add new-style input events (#310) --- src/SMAPI/Events/ControlEvents.cs | 28 ++++++------ src/SMAPI/Events/EventArgsInput.cs | 10 ++--- src/SMAPI/Events/IInputEvents.cs | 14 ++++++ src/SMAPI/Events/IModEvents.cs | 3 ++ src/SMAPI/Events/InputButtonPressedEventArgs.cs | 56 ++++++++++++++++++++++++ src/SMAPI/Events/InputButtonReleasedEventArgs.cs | 56 ++++++++++++++++++++++++ src/SMAPI/Events/InputEvents.cs | 8 ++-- src/SMAPI/Framework/Events/EventManager.cs | 48 ++++++++++++-------- src/SMAPI/Framework/Events/ModEvents.cs | 4 ++ src/SMAPI/Framework/Events/ModInputEvents.cs | 36 +++++++++++++++ src/SMAPI/Framework/SGame.cs | 20 +++++---- src/SMAPI/StardewModdingAPI.csproj | 4 ++ 12 files changed, 235 insertions(+), 52 deletions(-) create mode 100644 src/SMAPI/Events/IInputEvents.cs create mode 100644 src/SMAPI/Events/InputButtonPressedEventArgs.cs create mode 100644 src/SMAPI/Events/InputButtonReleasedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModInputEvents.cs (limited to 'src') diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 973bb245..77bbf3ab 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -20,22 +20,22 @@ namespace StardewModdingAPI.Events /// Raised when the changes. That happens when the player presses or releases a key. public static event EventHandler KeyboardChanged { - add => ControlEvents.EventManager.Control_KeyboardChanged.Add(value); - remove => ControlEvents.EventManager.Control_KeyboardChanged.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Remove(value); } /// Raised when the player presses a keyboard key. public static event EventHandler KeyPressed { - add => ControlEvents.EventManager.Control_KeyPressed.Add(value); - remove => ControlEvents.EventManager.Control_KeyPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_KeyPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_KeyPressed.Remove(value); } /// Raised when the player releases a keyboard key. public static event EventHandler KeyReleased { - add => ControlEvents.EventManager.Control_KeyReleased.Add(value); - remove => ControlEvents.EventManager.Control_KeyReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_KeyReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_KeyReleased.Remove(value); } /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. @@ -48,29 +48,29 @@ namespace StardewModdingAPI.Events /// The player pressed a controller button. This event isn't raised for trigger buttons. public static event EventHandler ControllerButtonPressed { - add => ControlEvents.EventManager.Control_ControllerButtonPressed.Add(value); - remove => ControlEvents.EventManager.Control_ControllerButtonPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Remove(value); } /// The player released a controller button. This event isn't raised for trigger buttons. public static event EventHandler ControllerButtonReleased { - add => ControlEvents.EventManager.Control_ControllerButtonReleased.Add(value); - remove => ControlEvents.EventManager.Control_ControllerButtonReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Remove(value); } /// The player pressed a controller trigger button. public static event EventHandler ControllerTriggerPressed { - add => ControlEvents.EventManager.Control_ControllerTriggerPressed.Add(value); - remove => ControlEvents.EventManager.Control_ControllerTriggerPressed.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Remove(value); } /// The player released a controller trigger button. public static event EventHandler ControllerTriggerReleased { - add => ControlEvents.EventManager.Control_ControllerTriggerReleased.Add(value); - remove => ControlEvents.EventManager.Control_ControllerTriggerReleased.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Remove(value); } diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index d60f4017..0cafdba5 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -23,10 +23,10 @@ namespace StardewModdingAPI.Events public ICursorPosition Cursor { get; } /// Whether the input should trigger actions on the affected tile. - public bool IsActionButton { get; } + public bool IsActionButton => this.Button.IsActionButton(); /// Whether the input should use tools on the affected tile. - public bool IsUseToolButton { get; } + public bool IsUseToolButton => this.Button.IsUseToolButton(); /// Whether a mod has indicated the key was already handled. public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); @@ -38,15 +38,11 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input should trigger actions on the affected tile. - /// Whether the input should use tools on the affected tile. /// The buttons to suppress. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton, HashSet suppressButtons) + public EventArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) { this.Button = button; this.Cursor = cursor; - this.IsActionButton = isActionButton; - this.IsUseToolButton = isUseToolButton; this.SuppressButtons = suppressButtons; } diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs new file mode 100644 index 00000000..92802fda --- /dev/null +++ b/src/SMAPI/Events/IInputEvents.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + public interface IInputEvents + { + /// Raised when the player presses a button on the keyboard, controller, or mouse. + event EventHandler ButtonPressed; + + /// Raised when the player releases a button on the keyboard, controller, or mouse. + event EventHandler ButtonReleased; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 99e5523f..16ec6557 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events raised when the player provides input using a controller, keyboard, or mouse. + IInputEvents Input { get; } + /// Events raised when something changes in the world. IWorldEvents World { get; } } diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs new file mode 100644 index 00000000..c8d55cd4 --- /dev/null +++ b/src/SMAPI/Events/InputButtonPressedEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed. + public class InputButtonPressedArgsInput : EventArgs + { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + /// Whether a mod has indicated the key was already handled. + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The buttons to suppress. + public InputButtonPressedArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + { + this.Button = button; + this.Cursor = cursor; + this.SuppressButtons = suppressButtons; + } + + /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. + public void SuppressButton() + { + this.SuppressButton(this.Button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void SuppressButton(SButton button) + { + this.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs new file mode 100644 index 00000000..863fab6a --- /dev/null +++ b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is released. + public class InputButtonReleasedArgsInput : EventArgs + { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + /// Whether a mod has indicated the key was already handled. + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The buttons to suppress. + public InputButtonReleasedArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + { + this.Button = button; + this.Cursor = cursor; + this.SuppressButtons = suppressButtons; + } + + /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. + public void SuppressButton() + { + this.SuppressButton(this.Button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void SuppressButton(SButton button) + { + this.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index 84d7ce5d..e62d6ee6 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -19,15 +19,15 @@ namespace StardewModdingAPI.Events /// Raised when the player presses a button on the keyboard, controller, or mouse. public static event EventHandler ButtonPressed { - add => InputEvents.EventManager.Input_ButtonPressed.Add(value); - remove => InputEvents.EventManager.Input_ButtonPressed.Remove(value); + add => InputEvents.EventManager.Legacy_Input_ButtonPressed.Add(value); + remove => InputEvents.EventManager.Legacy_Input_ButtonPressed.Remove(value); } /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. public static event EventHandler ButtonReleased { - add => InputEvents.EventManager.Input_ButtonReleased.Add(value); - remove => InputEvents.EventManager.Input_ButtonReleased.Remove(value); + add => InputEvents.EventManager.Legacy_Input_ButtonReleased.Add(value); + remove => InputEvents.EventManager.Legacy_Input_ButtonReleased.Remove(value); } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 100e4e43..62d9582e 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -32,6 +32,15 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; + /**** + ** Input + ****/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised when the player released a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + /********* ** Events (old) @@ -46,28 +55,28 @@ namespace StardewModdingAPI.Framework.Events ** ControlEvents ****/ /// Raised when the changes. That happens when the player presses or releases a key. - public readonly ManagedEvent Control_KeyboardChanged; + public readonly ManagedEvent Legacy_Control_KeyboardChanged; /// Raised when the player presses a keyboard key. - public readonly ManagedEvent Control_KeyPressed; + public readonly ManagedEvent Legacy_Control_KeyPressed; /// Raised when the player releases a keyboard key. - public readonly ManagedEvent Control_KeyReleased; + public readonly ManagedEvent Legacy_Control_KeyReleased; /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. public readonly ManagedEvent Control_MouseChanged; /// The player pressed a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Control_ControllerButtonPressed; + public readonly ManagedEvent Legacy_Control_ControllerButtonPressed; /// The player released a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Control_ControllerButtonReleased; + public readonly ManagedEvent Legacy_Control_ControllerButtonReleased; /// The player pressed a controller trigger button. - public readonly ManagedEvent Control_ControllerTriggerPressed; + public readonly ManagedEvent Legacy_Control_ControllerTriggerPressed; /// The player released a controller trigger button. - public readonly ManagedEvent Control_ControllerTriggerReleased; + public readonly ManagedEvent Legacy_Control_ControllerTriggerReleased; /**** ** GameEvents @@ -124,10 +133,10 @@ namespace StardewModdingAPI.Framework.Events ** InputEvents ****/ /// Raised when the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; + public readonly ManagedEvent Legacy_Input_ButtonPressed; /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; + public readonly ManagedEvent Legacy_Input_ButtonReleased; /**** ** LocationEvents @@ -234,6 +243,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); this.World_LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); @@ -244,13 +256,13 @@ namespace StardewModdingAPI.Framework.Events // init events (old) this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); - this.Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); - this.Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); - this.Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); - this.Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); - this.Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); - this.Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); - this.Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Legacy_Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Legacy_Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Legacy_Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Legacy_Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Legacy_Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Legacy_Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Legacy_Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); this.Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); @@ -270,8 +282,8 @@ namespace StardewModdingAPI.Framework.Events this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); - this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); - this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + this.Legacy_Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Legacy_Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); this.Legacy_Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); this.Legacy_Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index cc4cf8d7..90853141 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + public IInputEvents Input { get; } + /// Events raised when something changes in the world. public IWorldEvents World { get; } @@ -20,6 +23,7 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.Input = new ModInputEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); } } diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs new file mode 100644 index 00000000..18baec16 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -0,0 +1,36 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + internal class ModInputEvents : ModEventsBase, IInputEvents + { + /********* + ** Accessors + *********/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public event EventHandler ButtonPressed + { + add => this.EventManager.Input_ButtonPressed.Add(value); + remove => this.EventManager.Input_ButtonPressed.Remove(value); + } + + /// Raised when the player releases a button on the keyboard, controller, or mouse. + public event EventHandler ButtonReleased + { + add => this.EventManager.Input_ButtonReleased.Add(value); + remove => this.EventManager.Input_ButtonReleased.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModInputEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 1611861f..f87293c2 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -459,45 +459,47 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); + this.Events.Legacy_Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + this.Events.Legacy_Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.RealKeyboard != previousInputState.RealKeyboard) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 7b9629e2..1bdad1b5 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,7 +85,10 @@ Properties\GlobalAssemblyInfo.cs + + + @@ -107,6 +110,7 @@ + -- cgit From 6f931aa576b2b2f6a64e7e0522e01f6a37c92c8a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 02:35:26 -0400 Subject: add Input.CursorMoved event (#310) --- src/SMAPI/Events/ControlEvents.cs | 8 +++---- src/SMAPI/Events/IInputEvents.cs | 7 +++++-- src/SMAPI/Events/InputCursorMovedEventArgs.cs | 30 +++++++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 20 +++++++++++------- src/SMAPI/Framework/Events/ModInputEvents.cs | 11 ++++++++-- src/SMAPI/Framework/SGame.cs | 27 +++++++++++++++++------- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 src/SMAPI/Events/InputCursorMovedEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 77bbf3ab..a3994d1d 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -24,14 +24,14 @@ namespace StardewModdingAPI.Events remove => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Remove(value); } - /// Raised when the player presses a keyboard key. + /// Raised after the player presses a keyboard key. public static event EventHandler KeyPressed { add => ControlEvents.EventManager.Legacy_Control_KeyPressed.Add(value); remove => ControlEvents.EventManager.Legacy_Control_KeyPressed.Remove(value); } - /// Raised when the player releases a keyboard key. + /// Raised after the player releases a keyboard key. public static event EventHandler KeyReleased { add => ControlEvents.EventManager.Legacy_Control_KeyReleased.Add(value); @@ -41,8 +41,8 @@ namespace StardewModdingAPI.Events /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. public static event EventHandler MouseChanged { - add => ControlEvents.EventManager.Control_MouseChanged.Add(value); - remove => ControlEvents.EventManager.Control_MouseChanged.Remove(value); + add => ControlEvents.EventManager.Legacy_Control_MouseChanged.Add(value); + remove => ControlEvents.EventManager.Legacy_Control_MouseChanged.Remove(value); } /// The player pressed a controller button. This event isn't raised for trigger buttons. diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 92802fda..938c772b 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -5,10 +5,13 @@ namespace StardewModdingAPI.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. public interface IInputEvents { - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. event EventHandler ButtonPressed; - /// Raised when the player releases a button on the keyboard, controller, or mouse. + /// Raised after the player releases a button on the keyboard, controller, or mouse. event EventHandler ButtonReleased; + + /// Raised after the player moves the in-game cursor. + event EventHandler CursorMoved; } } diff --git a/src/SMAPI/Events/InputCursorMovedEventArgs.cs b/src/SMAPI/Events/InputCursorMovedEventArgs.cs new file mode 100644 index 00000000..02e1ee2c --- /dev/null +++ b/src/SMAPI/Events/InputCursorMovedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the in-game cursor is moved. + public class InputCursorMovedArgsInput : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous cursor position. + public ICursorPosition OldPosition { get; } + + /// The current cursor position. + public ICursorPosition NewPosition { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous cursor position. + /// The new cursor position. + public InputCursorMovedArgsInput(ICursorPosition oldPosition, ICursorPosition newPosition) + { + this.OldPosition = oldPosition; + this.NewPosition = newPosition; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 62d9582e..eea74587 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -35,12 +35,15 @@ namespace StardewModdingAPI.Framework.Events /**** ** Input ****/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. public readonly ManagedEvent Input_ButtonPressed; - /// Raised when the player released a button on the keyboard, controller, or mouse. + /// Raised after the player released a button on the keyboard, controller, or mouse. public readonly ManagedEvent Input_ButtonReleased; + /// Raised after the player moves the in-game cursor. + public readonly ManagedEvent Input_CursorMoved; + /********* ** Events (old) @@ -57,14 +60,14 @@ namespace StardewModdingAPI.Framework.Events /// Raised when the changes. That happens when the player presses or releases a key. public readonly ManagedEvent Legacy_Control_KeyboardChanged; - /// Raised when the player presses a keyboard key. + /// Raised after the player presses a keyboard key. public readonly ManagedEvent Legacy_Control_KeyPressed; - /// Raised when the player releases a keyboard key. + /// Raised after the player releases a keyboard key. public readonly ManagedEvent Legacy_Control_KeyReleased; /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public readonly ManagedEvent Control_MouseChanged; + public readonly ManagedEvent Legacy_Control_MouseChanged; /// The player pressed a controller button. This event isn't raised for trigger buttons. public readonly ManagedEvent Legacy_Control_ControllerButtonPressed; @@ -132,10 +135,10 @@ namespace StardewModdingAPI.Framework.Events /**** ** InputEvents ****/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. public readonly ManagedEvent Legacy_Input_ButtonPressed; - /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + /// Raised after the player releases a keyboard key on the keyboard, controller, or mouse. public readonly ManagedEvent Legacy_Input_ButtonReleased; /**** @@ -245,6 +248,7 @@ namespace StardewModdingAPI.Framework.Events // init events (new) this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); @@ -263,7 +267,7 @@ namespace StardewModdingAPI.Framework.Events this.Legacy_Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); this.Legacy_Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); this.Legacy_Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); - this.Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + this.Legacy_Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 18baec16..48dd0369 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -9,20 +9,27 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. + /// Raised after the player presses a button on the keyboard, controller, or mouse. public event EventHandler ButtonPressed { add => this.EventManager.Input_ButtonPressed.Add(value); remove => this.EventManager.Input_ButtonPressed.Remove(value); } - /// Raised when the player releases a button on the keyboard, controller, or mouse. + /// Raised after the player releases a button on the keyboard, controller, or mouse. public event EventHandler ButtonReleased { add => this.EventManager.Input_ButtonReleased.Add(value); remove => this.EventManager.Input_ButtonReleased.Remove(value); } + /// Raised after the player moves the in-game cursor. + public event EventHandler CursorMoved + { + add => this.EventManager.Input_CursorMoved.Add(value); + remove => this.EventManager.Input_CursorMoved.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index f87293c2..f4e0d3c5 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -103,6 +103,9 @@ namespace StardewModdingAPI.Framework /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; + /// The previous cursor position. + private ICursorPosition PreviousCursorPosition; + /// An index incremented on every tick and reset every 60th tick (0–59). private int CurrentUpdateTick; @@ -444,14 +447,24 @@ namespace StardewModdingAPI.Framework { // cursor position Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); + if (this.PreviousCursorPosition == null || screenPixels != this.PreviousCursorPosition.ScreenPixels) + { + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); + } + else + cursor = this.PreviousCursorPosition; } - // raise input events + // raise cursor moved event + if (this.PreviousCursorPosition != null && cursor.ScreenPixels != this.PreviousCursorPosition.ScreenPixels) + this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor)); + this.PreviousCursorPosition = cursor; + + // raise input button events foreach (var pair in inputState.ActiveButtons) { SButton button = pair.Key; @@ -501,7 +514,7 @@ namespace StardewModdingAPI.Framework if (inputState.RealKeyboard != previousInputState.RealKeyboard) this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); + this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 1bdad1b5..604ad64f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 74971f532822b44fbffc99810afd2c0d8a0d424d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 03:00:39 -0400 Subject: use value watcher for cursor position (#310) --- src/SMAPI/Framework/SGame.cs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index f4e0d3c5..894a771f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -100,6 +100,9 @@ namespace StardewModdingAPI.Framework /// Tracks changes to . private readonly IValueWatcher ActiveMenuWatcher; + /// Tracks changes to the cursor position. + private readonly IValueWatcher CursorWatcher; + /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -168,6 +171,7 @@ namespace StardewModdingAPI.Framework // init watchers Game1.locations = new ObservableCollection(); + this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.MousePosition); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); @@ -175,6 +179,7 @@ namespace StardewModdingAPI.Framework this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations); this.Watchers.AddRange(new IWatcher[] { + this.CursorWatcher, this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, @@ -443,25 +448,24 @@ namespace StardewModdingAPI.Framework if (!isChatInput) { // get cursor position - ICursorPosition cursor; + ICursorPosition cursor = this.PreviousCursorPosition; + if (this.CursorWatcher.IsChanged) { // cursor position - Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - if (this.PreviousCursorPosition == null || screenPixels != this.PreviousCursorPosition.ScreenPixels) - { - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); - } - else - cursor = this.PreviousCursorPosition; + Vector2 screenPixels = new Vector2(this.CursorWatcher.CurrentValue.X, this.CursorWatcher.CurrentValue.Y); + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); } // raise cursor moved event - if (this.PreviousCursorPosition != null && cursor.ScreenPixels != this.PreviousCursorPosition.ScreenPixels) + if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) + { + this.CursorWatcher.Reset(); this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor)); + } this.PreviousCursorPosition = cursor; // raise input button events -- cgit From 90f55a6921ac798e03d6f81240d3a9899544c031 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 12:14:15 -0400 Subject: add mouse scroll event (#310) --- src/SMAPI/Events/IInputEvents.cs | 3 ++ .../Events/InputMouseWheelScrolledEventArgs.cs | 38 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 4 +++ src/SMAPI/Framework/Events/ModInputEvents.cs | 7 ++++ src/SMAPI/Framework/SGame.cs | 15 +++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 68 insertions(+) create mode 100644 src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs (limited to 'src') diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 938c772b..64d82c57 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -13,5 +13,8 @@ namespace StardewModdingAPI.Events /// Raised after the player moves the in-game cursor. event EventHandler CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + event EventHandler MouseWheelScrolled; } } diff --git a/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs new file mode 100644 index 00000000..9afab9cc --- /dev/null +++ b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the player scrolls the mouse wheel. + public class InputMouseWheelScrolledEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The cursor position. + public ICursorPosition Position { get; } + + /// The old scroll value. + public int OldValue { get; } + + /// The new scroll value. + public int NewValue { get; } + + /// The amount by which the scroll value changed. + public int Delta => this.NewValue - this.OldValue; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cursor position. + /// The old scroll value. + /// The new scroll value. + public InputMouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue) + { + this.Position = position; + this.OldValue = oldValue; + this.NewValue = newValue; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index eea74587..9f67244a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -44,6 +44,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player moves the in-game cursor. public readonly ManagedEvent Input_CursorMoved; + /// Raised after the player scrolls the mouse wheel. + public readonly ManagedEvent Input_MouseWheelScrolled; + /********* ** Events (old) @@ -249,6 +252,7 @@ namespace StardewModdingAPI.Framework.Events this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.Input_MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); this.World_LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 48dd0369..387ea87a 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -30,6 +30,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.Input_CursorMoved.Remove(value); } + /// Raised after the player scrolls the mouse wheel. + public event EventHandler MouseWheelScrolled + { + add => this.EventManager.Input_MouseWheelScrolled.Add(value); + remove => this.EventManager.Input_MouseWheelScrolled.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 894a771f..18529728 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -103,6 +103,9 @@ namespace StardewModdingAPI.Framework /// Tracks changes to the cursor position. private readonly IValueWatcher CursorWatcher; + /// Tracks changes to the mouse wheel scroll. + private readonly IValueWatcher MouseWheelScrollWatcher; + /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -172,6 +175,7 @@ namespace StardewModdingAPI.Framework // init watchers Game1.locations = new ObservableCollection(); this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.MousePosition); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); @@ -180,6 +184,7 @@ namespace StardewModdingAPI.Framework this.Watchers.AddRange(new IWatcher[] { this.CursorWatcher, + this.MouseWheelScrollWatcher, this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, @@ -468,6 +473,16 @@ namespace StardewModdingAPI.Framework } this.PreviousCursorPosition = cursor; + // raise mouse wheel scrolled + if (this.MouseWheelScrollWatcher.IsChanged) + { + int oldValue = this.MouseWheelScrollWatcher.PreviousValue; + int newValue = this.MouseWheelScrollWatcher.CurrentValue; + this.MouseWheelScrollWatcher.Reset(); + + this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, oldValue, newValue)); + } + // raise input button events foreach (var pair in inputState.ActiveButtons) { diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 604ad64f..b81f1359 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 33db019c621a3f09fcfd5a79141831a63e14dedd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 15:48:35 -0400 Subject: update various wiki links --- docs/release-notes.md | 10 +++++----- src/SMAPI.Web/Startup.cs | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e93c6bff..750fa37f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -261,7 +261,7 @@ * **New features for modders** SMAPI 2.0 adds several features to enable new kinds of mods (see - [API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)). + [API documentation](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs)). The **content API** lets you edit, inject, and reload XNB data loaded by the game at any time. This lets SMAPI mods do anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g. @@ -384,8 +384,8 @@ For players: * Updated mod compatibility list. For modders: -* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)). -* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). +* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Utilities#Dates)). +* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)). * Added more useful logging when loading mods. * Added a `ModID` property to all mod helpers for extension methods. * Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code. @@ -417,8 +417,8 @@ For players: * Updated mod compatibility list. For modders: -* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). -* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)). +* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)). +* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation)). * You can now load unpacked `.tbin` files from your mod folder through the content API. * SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 7f5b8e2b..82a0f4f6 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -161,16 +161,15 @@ namespace StardewModdingAPI.Web // redirect legacy canimod.com URLs var wikiRedirects = new Dictionary { - ["Modding:Creating_a_SMAPI_mod"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod" }, + ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" }, + ["Modding:Modder_Guide"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod", "^/for-devs/creating-a-smapi-mod-advanced-config" }, + ["Modding:Player_Guide"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods", "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" }, + ["Modding:Editing_XNB_files"] = new[] { "^/for-devs/creating-an-xnb-mod", "^/guides/creating-an-xnb-mod" }, ["Modding:Event_data"] = new[] { "^/for-devs/events", "^/guides/events" }, ["Modding:Gift_taste_data"] = new[] { "^/for-devs/npc-gift-tastes", "^/guides/npc-gift-tastes" }, ["Modding:IDE_reference"] = new[] { "^/for-devs/creating-a-smapi-mod-ide-primer" }, - ["Modding:Installing_SMAPI"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods" }, ["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" }, - ["Modding:Player_FAQs"] = new[] { "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" }, - ["Modding:SMAPI_APIs"] = new[] { "^/for-devs/creating-a-smapi-mod-advanced-config" }, - ["Modding:Updating_deprecated_SMAPI_code"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" }, ["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" } }; foreach (KeyValuePair pair in wikiRedirects) -- cgit From d41fe6ff88b569f991f219c5f348d3688fba956f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 16:00:16 -0400 Subject: add input API --- docs/release-notes.md | 1 + src/SMAPI/Framework/CursorPosition.cs | 7 +++- src/SMAPI/Framework/Input/SInputState.cs | 28 +++++++++++--- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 54 +++++++++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 8 +++- src/SMAPI/Framework/SGame.cs | 25 ++++--------- src/SMAPI/IInputHelper.cs | 21 +++++++++++ src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 + 9 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/InputHelper.cs create mode 100644 src/SMAPI/IInputHelper.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 750fa37f..8824c0fb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * For modders: + * Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input. * Added code analysis to mod build config package to flag common issues as warnings. * Replaced `LocationEvents` with a more powerful set of events for multiplayer: * now raised for all locations; diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index db02b3d1..6f716746 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// The raw pixel position, not adjusted for the game zoom. + public Vector2 RawPixels { get; } + /// The pixel position relative to the top-left corner of the visible screen. public Vector2 ScreenPixels { get; } @@ -22,11 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. + /// The raw pixel position, not adjusted for the game zoom. /// The pixel position relative to the top-left corner of the visible screen. /// The tile position relative to the top-left corner of the map. /// The tile position that the game considers under the cursor for purposes of clicking actions. - public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + public CursorPosition(Vector2 rawPixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) { + this.RawPixels = rawPixels; this.ScreenPixels = screenPixels; this.Tile = tile; this.GrabTile = grabTile; diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 27e40ab4..44fd0618 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -8,7 +8,7 @@ using StardewValley; #pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) namespace StardewModdingAPI.Framework.Input { - /// A summary of input changes during an update frame. + /// Manages the game's input state. internal sealed class SInputState : InputState { /********* @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Input /// The maximum amount of direction to ignore for the left thumbstick. private const float LeftThumbstickDeadZone = 0.2f; + /// The cursor position on the screen adjusted for the zoom level. + private CursorPosition CursorPositionImpl; + /********* ** Accessors @@ -39,8 +42,8 @@ namespace StardewModdingAPI.Framework.Input /// A derivative of which suppresses the buttons in . public MouseState SuppressedMouse { get; private set; } - /// The mouse position on the screen adjusted for the zoom level. - public Point MousePosition { get; private set; } + /// The cursor position on the screen adjusted for the zoom level. + public ICursorPosition CursorPosition => this.CursorPositionImpl; /// The buttons which were pressed, held, or released. public IDictionary ActiveButtons { get; private set; } = new Dictionary(); @@ -61,7 +64,7 @@ namespace StardewModdingAPI.Framework.Input RealController = this.RealController, RealKeyboard = this.RealKeyboard, RealMouse = this.RealMouse, - MousePosition = this.MousePosition + CursorPositionImpl = this.CursorPositionImpl }; } @@ -78,15 +81,16 @@ namespace StardewModdingAPI.Framework.Input GamePadState realController = GamePad.GetState(PlayerIndex.One); KeyboardState realKeyboard = Keyboard.GetState(); MouseState realMouse = Mouse.GetState(); - Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); + Vector2 cursorRawPixelPos = new Vector2(this.RealMouse.X, this.RealMouse.Y); // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - this.MousePosition = mousePosition; + if (this.CursorPositionImpl?.RawPixels != cursorRawPixelPos) + this.CursorPositionImpl = this.GetCursorPosition(cursorRawPixelPos); // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); @@ -157,6 +161,18 @@ namespace StardewModdingAPI.Framework.Input /********* ** Private methods *********/ + /// Get the current cursor position. + /// The raw pixel position from the mouse state. + private CursorPosition GetCursorPosition(Vector2 rawPixelPos) + { + Vector2 screenPixels = new Vector2((int)(rawPixelPos.X * (1.0 / Game1.options.zoomLevel)), (int)(rawPixelPos.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + return new CursorPosition(rawPixelPos, screenPixels, tile, grabTile); + } + /// Whether input should be suppressed in the current context. private bool ShouldSuppressNow() { diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs new file mode 100644 index 00000000..f4cd12b6 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -0,0 +1,54 @@ +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for checking and changing input state. + internal class InputHelper : BaseHelper, IInputHelper + { + /********* + ** Accessors + *********/ + /// Manages the game's input state. + private readonly SInputState InputState; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// Manages the game's input state. + public InputHelper(string modID, SInputState inputState) + : base(modID) + { + this.InputState = inputState; + } + + /// Get the current cursor position. + public ICursorPosition GetCursorPosition() + { + return this.InputState.CursorPosition; + } + + /// Get whether a button is currently pressed. + /// The button. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void Suppress(SButton button) + { + this.InputState.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 92cb9d94..1e07dafa 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; @@ -40,6 +41,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for loading content assets. public IContentHelper Content { get; } + /// An API for checking and changing input state. + public IInputHelper Input { get; } + /// An API for accessing private game code. public IReflectionHelper Reflection { get; } @@ -63,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod's unique ID. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. + /// Manages the game's input state. /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. @@ -75,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -88,6 +93,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DirectoryPath = modDirectory; this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.Input = new InputHelper(modID, inputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 18529728..560b54a4 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -54,9 +54,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; - /// Manages input visible to the game. - private SInputState Input => (SInputState)Game1.input; - /// 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 @@ -101,7 +98,7 @@ namespace StardewModdingAPI.Framework private readonly IValueWatcher ActiveMenuWatcher; /// Tracks changes to the cursor position. - private readonly IValueWatcher CursorWatcher; + private readonly IValueWatcher CursorWatcher; /// Tracks changes to the mouse wheel scroll. private readonly IValueWatcher MouseWheelScrollWatcher; @@ -137,6 +134,9 @@ namespace StardewModdingAPI.Framework /// SMAPI's content manager. public ContentCoordinator ContentCore { get; private set; } + /// Manages input visible to the game. + public SInputState Input => (SInputState)Game1.input; + /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Framework // init watchers Game1.locations = new ObservableCollection(); - this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.MousePosition); + this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels); this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); @@ -452,18 +452,7 @@ namespace StardewModdingAPI.Framework bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); if (!isChatInput) { - // get cursor position - ICursorPosition cursor = this.PreviousCursorPosition; - if (this.CursorWatcher.IsChanged) - { - // cursor position - Vector2 screenPixels = new Vector2(this.CursorWatcher.CurrentValue.X, this.CursorWatcher.CurrentValue.Y); - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); - } + ICursorPosition cursor = this.Input.CursorPosition; // raise cursor moved event if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) @@ -533,7 +522,7 @@ namespace StardewModdingAPI.Framework if (inputState.RealKeyboard != previousInputState.RealKeyboard) this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); + this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); } } diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs new file mode 100644 index 00000000..328f504b --- /dev/null +++ b/src/SMAPI/IInputHelper.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI +{ + /// Provides an API for checking and changing input state. + public interface IInputHelper : IModLinked + { + /// Get the current cursor position. + ICursorPosition GetCursorPosition(); + + /// Get whether a button is currently pressed. + /// The button. + bool IsDown(SButton button); + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + bool IsSuppressed(SButton button); + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + void Suppress(SButton button); + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 48ad922b..76c12351 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -855,7 +855,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index b81f1359..f4aee551 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -114,6 +114,8 @@ + + -- cgit From c0ba24456ba741ad5b072c033f9b0537ee74ad04 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 16:15:49 -0400 Subject: remove suppression from new events, add convenience methods (#310) Mods should use the new input API to suppress input instead. --- src/SMAPI/Events/InputButtonPressedEventArgs.cs | 36 +++++++++++++----------- src/SMAPI/Events/InputButtonReleasedEventArgs.cs | 36 +++++++++++++----------- src/SMAPI/Framework/SGame.cs | 4 +-- 3 files changed, 42 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs index c8d55cd4..002f7cf1 100644 --- a/src/SMAPI/Events/InputButtonPressedEventArgs.cs +++ b/src/SMAPI/Events/InputButtonPressedEventArgs.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Events { @@ -9,8 +9,8 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ - /// The buttons to suppress. - private readonly HashSet SuppressButtons; + /// The game's current input state. + private readonly SInputState InputState; /********* @@ -22,9 +22,6 @@ namespace StardewModdingAPI.Events /// The current cursor position. public ICursorPosition Cursor { get; } - /// Whether a mod has indicated the key was already handled. - public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); - /********* ** Public methods @@ -32,25 +29,32 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// The buttons to suppress. - public InputButtonPressedArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + /// The game's current input state. + internal InputButtonPressedArgsInput(SButton button, ICursorPosition cursor, SInputState inputState) { this.Button = button; this.Cursor = cursor; - this.SuppressButtons = suppressButtons; + this.InputState = inputState; + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + public bool IsSuppressed() + { + return this.IsSuppressed(this.Button); } - /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. - public void SuppressButton() + /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// The button to check. + public bool IsSuppressed(SButton button) { - this.SuppressButton(this.Button); + return this.InputState.SuppressButtons.Contains(button); } - /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. - /// The button to suppress. - public void SuppressButton(SButton button) + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) { - this.SuppressButtons.Add(button); + return this.InputState.IsDown(button); } } } diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs index 863fab6a..bc5e4a89 100644 --- a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs +++ b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Events { @@ -9,8 +9,8 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ - /// The buttons to suppress. - private readonly HashSet SuppressButtons; + /// The game's current input state. + private readonly SInputState InputState; /********* @@ -22,9 +22,6 @@ namespace StardewModdingAPI.Events /// The current cursor position. public ICursorPosition Cursor { get; } - /// Whether a mod has indicated the key was already handled. - public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); - /********* ** Public methods @@ -32,25 +29,32 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// The buttons to suppress. - public InputButtonReleasedArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + /// The game's current input state. + internal InputButtonReleasedArgsInput(SButton button, ICursorPosition cursor, SInputState inputState) { this.Button = button; this.Cursor = cursor; - this.SuppressButtons = suppressButtons; + this.InputState = inputState; + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + public bool IsSuppressed() + { + return this.IsSuppressed(this.Button); } - /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. - public void SuppressButton() + /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// The button to check. + public bool IsSuppressed(SButton button) { - this.SuppressButton(this.Button); + return this.InputState.SuppressButtons.Contains(button); } - /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. - /// The button to suppress. - public void SuppressButton(SButton button) + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) { - this.SuppressButtons.Add(button); + return this.InputState.IsDown(button); } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 560b54a4..ae80f680 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -480,7 +480,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState)); this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events @@ -499,7 +499,7 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState)); this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events -- cgit From 2e3c42130358734a6fcf547745324dd272176f9c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 17:43:51 -0400 Subject: tweak SGame update logic to avoid some edge cases (#310) --- src/SMAPI/Framework/SGame.cs | 103 +++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 44 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index ae80f680..a4d149f3 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -73,6 +73,15 @@ namespace StardewModdingAPI.Framework /// Whether the game is creating the save file and SMAPI has already raised . private bool IsBetweenCreateEvents; + /// A callback to invoke after the game finishes initialising. + private readonly Action OnGameInitialised; + + /// A callback to invoke when the game exits. + private readonly Action OnGameExiting; + + /// Simplifies access to private game code. + private readonly Reflector Reflection; + /**** ** Game state ****/ @@ -80,28 +89,28 @@ namespace StardewModdingAPI.Framework private readonly List Watchers = new List(); /// Tracks changes to the window size. - private readonly IValueWatcher WindowSizeWatcher; + private IValueWatcher WindowSizeWatcher; /// Tracks changes to the current player. private PlayerTracker CurrentPlayerTracker; /// Tracks changes to the time of day (in 24-hour military format). - private readonly IValueWatcher TimeWatcher; + private IValueWatcher TimeWatcher; /// Tracks changes to the save ID. - private readonly IValueWatcher SaveIdWatcher; + private IValueWatcher SaveIdWatcher; /// Tracks changes to the game's locations. - private readonly WorldLocationsTracker LocationsWatcher; + private WorldLocationsTracker LocationsWatcher; /// Tracks changes to . - private readonly IValueWatcher ActiveMenuWatcher; + private IValueWatcher ActiveMenuWatcher; /// Tracks changes to the cursor position. - private readonly IValueWatcher CursorWatcher; + private IValueWatcher CursorWatcher; /// Tracks changes to the mouse wheel scroll. - private readonly IValueWatcher MouseWheelScrollWatcher; + private IValueWatcher MouseWheelScrollWatcher; /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -112,18 +121,12 @@ namespace StardewModdingAPI.Framework /// An index incremented on every tick and reset every 60th tick (0–59). private int CurrentUpdateTick; + /// Whether post-game-startup initialisation has been performed. + private bool IsInitialised; + /// Whether this is the very first update tick since the game started. private bool FirstUpdate; - /// A callback to invoke after the game finishes initialising. - private readonly Action OnGameInitialised; - - /// A callback to invoke when the game exits. - private readonly Action OnGameExiting; - - /// Simplifies access to private game code. - private readonly Reflector Reflection; - /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; @@ -172,8 +175,17 @@ namespace StardewModdingAPI.Framework Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); - // init watchers + // init observables Game1.locations = new ObservableCollection(); + } + + /// Initialise just before the game's first update tick. + private void InitialiseAfterGameStarted() + { + // set initial state + this.Input.TrueUpdate(); + + // init watchers this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels); this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); @@ -191,6 +203,9 @@ namespace StardewModdingAPI.Framework this.ActiveMenuWatcher, this.LocationsWatcher }); + + // raise callback + this.OnGameInitialised(); } /// Perform cleanup logic when the game exits. @@ -239,19 +254,24 @@ namespace StardewModdingAPI.Framework try { /********* - ** Update input + ** Special cases *********/ - // This should *always* run, even when suppressing mod events, since the game uses - // this too. For example, doing this after mod event suppression would prevent the - // user from doing anything on the overnight shipping screen. - SInputState previousInputState = this.Input.Clone(); - SInputState inputState = this.Input; - if (this.IsActive) - inputState.TrueUpdate(); + // Perform first-tick initialisation. + if (!this.IsInitialised) + { + this.IsInitialised = true; + this.InitialiseAfterGameStarted(); + } - /********* - ** Load game synchronously - *********/ + // Abort if SMAPI is exiting. + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); + return; + } + + // Load saves synchronously to avoid issues due to mod events triggering + // concurrently with game code. if (Game1.gameMode == Game1.loadingMode) { this.Monitor.Log("Running game loader...", LogLevel.Trace); @@ -263,16 +283,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Game loader OK.", LogLevel.Trace); } - /********* - ** Skip conditions - *********/ - // SMAPI exiting, stop processing game updates - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); - return; - } - // While a background task is in progress, the game may make changes to the game // state while mods are running their code. This is risky, because data changes can // conflict (e.g. collection changed during enumeration errors) and data may change @@ -289,6 +299,17 @@ namespace StardewModdingAPI.Framework return; } + /********* + ** Update input + *********/ + // This should *always* run, even when suppressing mod events, since the game uses + // this too. For example, doing this after mod event suppression would prevent the + // user from doing anything on the overnight shipping screen. + SInputState previousInputState = this.Input.Clone(); + SInputState inputState = this.Input; + if (this.IsActive) + inputState.TrueUpdate(); + /********* ** Save events + suppress events during save *********/ @@ -336,12 +357,6 @@ namespace StardewModdingAPI.Framework this.Events.Time_AfterDayStarted.Raise(); } - /********* - ** Notify SMAPI that game is initialised - *********/ - if (this.FirstUpdate) - this.OnGameInitialised(); - /********* ** Update context *********/ -- cgit From de74b038e4c15d393de8828c89814ef7eaefe507 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 18:22:04 -0400 Subject: move web API client into toolkit (#532) --- src/SMAPI.Internal/Models/ModInfoModel.cs | 56 ----------------- src/SMAPI.Internal/Models/ModSeachModel.cs | 37 ----------- src/SMAPI.Internal/SMAPI.Internal.projitems | 2 - src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 2 +- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI/Constants.cs | 16 ++++- src/SMAPI/Framework/WebApiClient.cs | 73 ---------------------- src/SMAPI/Program.cs | 4 +- src/SMAPI/SemanticVersion.cs | 19 +++--- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Framework/Clients/WebApi/ModInfoModel.cs | 56 +++++++++++++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 37 +++++++++++ .../Framework/Clients/WebApi/WebApiClient.cs | 70 +++++++++++++++++++++ 17 files changed, 192 insertions(+), 191 deletions(-) delete mode 100644 src/SMAPI.Internal/Models/ModInfoModel.cs delete mode 100644 src/SMAPI.Internal/Models/ModSeachModel.cs delete mode 100644 src/SMAPI/Framework/WebApiClient.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs (limited to 'src') diff --git a/src/SMAPI.Internal/Models/ModInfoModel.cs b/src/SMAPI.Internal/Models/ModInfoModel.cs deleted file mode 100644 index 725c88bb..00000000 --- a/src/SMAPI.Internal/Models/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Internal.Models -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The semantic version for the mod's latest release. - public string Version { get; set; } - - /// The semantic version for the mod's latest preview release, if available and different from . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/SMAPI.Internal/Models/ModSeachModel.cs b/src/SMAPI.Internal/Models/ModSeachModel.cs deleted file mode 100644 index fac72135..00000000 --- a/src/SMAPI.Internal/Models/ModSeachModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Internal.Models -{ - /// Specifies mods whose update-check info to fetch. - internal class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) - { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; - } - } -} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 33b8cbfa..54b12003 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -12,8 +12,6 @@ - - diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index fc90d067..1ec855d5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index bd27d624..4a4a40cd 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 2782e2b9..e6074a60 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index f4abd379..1d7e4fff 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 79fe8f87..4c879c8d 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 87a87ab7..6cac6b8f 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bdbd5fbb..786c1a39 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.15"); + public static ISemanticVersion ApiVersion { get; } /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.13"); + public static ISemanticVersion MinimumGameVersion { get; } /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -70,6 +70,9 @@ namespace StardewModdingAPI /**** ** Internal ****/ + /// SMAPI's current semantic version as a mod toolkit version. + internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } + /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; @@ -104,6 +107,15 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ + /// Initialise the static values. + static Constants() + { + Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.15"); + Constants.MinimumGameVersion = new GameVersion("1.3.13"); + + Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); + } + /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs deleted file mode 100644 index e33b2681..00000000 --- a/src/SMAPI/Framework/WebApiClient.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Newtonsoft.Json; -using StardewModdingAPI.Internal.Models; - -namespace StardewModdingAPI.Framework -{ - /// Provides methods for interacting with the SMAPI web API. - internal class WebApiClient - { - /********* - ** Properties - *********/ - /// The base URL for the web API. - private readonly Uri BaseUrl; - - /// The API version number. - private readonly ISemanticVersion Version; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The base URL for the web API. - /// The web API version. - public WebApiClient(string baseUrl, ISemanticVersion version) - { -#if !SMAPI_FOR_WINDOWS - baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif - this.BaseUrl = new Uri(baseUrl); - this.Version = version; - } - - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) - { - return this.Post>( - $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) - ); - } - - - /********* - ** Private methods - *********/ - /// Fetch the response from the backend API. - /// The body content type. - /// The expected response type. - /// The request URL, optionally excluding the base URL. - /// The body content to post. - private TResult Post(string url, TBody content) - { - /*** - ** Note: avoid HttpClient for Mac compatibility. - ***/ - using (WebClient client = new WebClient()) - { - Uri fullUrl = new Uri(this.BaseUrl, url); - string data = JsonConvert.SerializeObject(content); - - client.Headers["Content-Type"] = "application/json"; - client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; - string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response); - } - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d5f5fdcd..b7b4dfc7 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -26,7 +26,7 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -561,7 +561,7 @@ namespace StardewModdingAPI new Thread(() => { // create client - WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersionForToolkit); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 3ee3ccf3..c4dd1912 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -54,6 +54,13 @@ namespace StardewModdingAPI public SemanticVersion(Version version) : this(new Toolkit.SemanticVersion(version)) { } + /// Construct an instance. + /// The underlying semantic version implementation. + internal SemanticVersion(Toolkit.ISemanticVersion version) + { + this.Version = version; + } + /// Whether this is a pre-release version. public bool IsPrerelease() { @@ -146,17 +153,5 @@ namespace StardewModdingAPI parsed = null; return false; } - - - /********* - ** Private methods - *********/ - /// Construct an instance. - /// The underlying semantic version implementation. - private SemanticVersion(Toolkit.ISemanticVersion version) - { - this.Version = version; - } - } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 96b3aa5b..a5ccc62d 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -279,7 +279,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs new file mode 100644 index 00000000..e1a204c6 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Generic metadata about a mod. + public class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The semantic version for the mod's latest release. + public string Version { get; set; } + + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs new file mode 100644 index 00000000..c0ee34ea --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies mods whose update-check info to fetch. + public class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + /// Whether to allow non-semantic versions, instead of returning an error for those. + public bool AllowInvalidVersions { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The namespaced mod keys to search. + /// Whether to allow non-semantic versions, instead of returning an error for those. + public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + { + this.ModKeys = modKeys.ToArray(); + this.AllowInvalidVersions = allowInvalidVersions; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs new file mode 100644 index 00000000..277fbeeb --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Provides methods for interacting with the SMAPI web API. + internal class WebApiClient + { + /********* + ** Properties + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) + { +#if !SMAPI_FOR_WINDOWS + baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } + + /// Get the latest SMAPI version. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params string[] modKeys) + { + return this.Post>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys, allowInvalidVersions: true) + ); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The body content type. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + /// The body content to post. + private TResult Post(string url, TBody content) + { + // note: avoid HttpClient for Mac compatibility + using (WebClient client = new WebClient()) + { + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response); + } + } + } +} -- cgit From 045891131ccfdb980fcd84b3d3e52a2b2fcd94e2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 3 Jun 2018 13:05:23 -0400 Subject: change 'starting game...' log level to Debug This makes the message visible to players, to avoid confusion where SMAPI seems to be doing nothing (especially after a pre-load prompt). --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b7b4dfc7..5c5137ef 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -270,7 +270,7 @@ namespace StardewModdingAPI } // start game - this.Monitor.Log("Starting game...", LogLevel.Trace); + this.Monitor.Log("Starting game...", LogLevel.Debug); try { this.IsGameRunning = true; -- cgit From a463a05607c89922af7e908b39aa897b8d23bfbf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 3 Jun 2018 13:54:26 -0400 Subject: redesign log parser upload page This makes the instructions much more clear and prominent, so it should be more intuitive for players. The previous design often confused users because they saw the big textbox and ignored the little instructions above it. --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/LogParserController.cs | 31 +++-- src/SMAPI.Web/ViewModels/LogParserModel.cs | 38 ++++++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 153 +++++++++++++---------- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 110 +++------------- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 90 ++++--------- 6 files changed, 193 insertions(+), 230 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8824c0fb..7668dc57 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -51,6 +51,7 @@ * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * For the log parser: + * Redesigned upload page to make it more intuitive for new players. * Fixed issue parsing content packs with no description. * For SMAPI developers: diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 62547deb..2bff1392 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Compression; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -72,13 +73,27 @@ namespace StardewModdingAPI.Web.Controllers ** JSON ***/ /// Save raw log data. - /// The log content to save. - [HttpPost, Produces("application/json"), AllowLargePosts] - [Route("log/save")] - public async Task PostAsync([FromBody] string content) + [HttpPost, AllowLargePosts] + [Route("log")] + public async Task PostAsync() { - content = this.CompressString(content); - return await this.Pastebin.PostAsync(content); + // get raw log text + string input = this.Request.Form["input"].FirstOrDefault(); + if (string.IsNullOrWhiteSpace(input)) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null, null) { UploadError = "The log file seems to be empty." }); + + // upload log + input = this.CompressString(input); + SavePasteResult result = await this.Pastebin.PostAsync(input); + + // handle errors + if (!result.Success) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID, null) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); + + // redirect to view + UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl)); + uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID; + return this.Redirect(uri.Uri.ToString()); } @@ -115,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers } // prefix length - var zipBuffer = new byte[compressedData.Length + 4]; + byte[] zipBuffer = new byte[compressedData.Length + 4]; Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); @@ -151,7 +166,7 @@ namespace StardewModdingAPI.Web.Controllers memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); // read data - var buffer = new byte[dataLength]; + byte[] buffer = new byte[dataLength]; memoryStream.Position = 0; using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) gZipStream.Read(buffer, 0, buffer.Length); diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index 8c026536..0fbd8ad5 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.ViewModels @@ -5,6 +8,13 @@ namespace StardewModdingAPI.Web.ViewModels /// The view model for the log parser page. public class LogParserModel { + /********* + ** Properties + *********/ + /// A regex pattern matching characters to remove from a mod name to create the slug ID. + private readonly Regex SlugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /********* ** Accessors *********/ @@ -17,6 +27,12 @@ namespace StardewModdingAPI.Web.ViewModels /// The parsed log info. public ParsedLog ParsedLog { get; set; } + /// An error which occurred while uploading the log to Pastebin. + public string UploadError { get; set; } + + /// An error which occurred while parsing the log file. + public string ParseError => this.ParsedLog?.Error; + /********* ** Public methods @@ -34,5 +50,27 @@ namespace StardewModdingAPI.Web.ViewModels this.PasteID = pasteID; this.ParsedLog = parsedLog; } + + /// Get all content packs in the log grouped by the mod they're for. + public IDictionary GetContentPacksByMod() + { + // get all mods & content packs + LogModInfo[] mods = this.ParsedLog?.Mods; + if (mods == null || !mods.Any()) + return new Dictionary(); + + // group by mod + return mods + .Where(mod => mod.ContentPackFor != null) + .GroupBy(mod => mod.ContentPackFor) + .ToDictionary(group => group.Key, group => group.ToArray()); + } + + /// Get a sanitised mod name that's safe to use in anchors, attributes, and URLs. + /// The mod name. + public string GetSlug(string modName) + { + return this.SlugInvalidCharPattern.Replace(modName, ""); + } } } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d051026f..79cd7a2b 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,23 +1,14 @@ -@{ - ViewData["Title"] = "SMAPI log parser"; - - IDictionary contentPacks = Model.ParsedLog?.Mods - ?.GroupBy(mod => mod.ContentPackFor) - .Where(group => group.Key != null) - .ToDictionary(group => group.Key, group => group.ToArray()); - - Regex slugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - string GetSlug(string modName) - { - return slugInvalidCharPattern.Replace(modName, ""); - } -} -@using System.Text.RegularExpressions @using Newtonsoft.Json @using StardewModdingAPI.Web.Framework.LogParsing.Models @model StardewModdingAPI.Web.ViewModels.LogParserModel + +@{ + ViewData["Title"] = "SMAPI log parser"; + IDictionary contentPacks = Model.GetContentPacksByMod(); +} + @section Head { - + @@ -26,51 +17,104 @@ smapi.logParser({ logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)), showPopup: @Json.Serialize(Model.ParsedLog == null), - showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }), + showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }), showLevels: { - trace: false, - debug: false, - info: true, - alert: true, - warn: true, - error: true + @LogLevel.Trace.ToString().ToLower(): false, + @LogLevel.Debug.ToString().ToLower(): false, + @LogLevel.Info.ToString().ToLower(): true, + @LogLevel.Alert.ToString().ToLower(): true, + @LogLevel.Warn.ToString().ToLower(): true, + @LogLevel.Error.ToString().ToLower(): true } }, '@Model.SectionUrl'); }); } -@********* -** Intro -*********@ +@* intro and upload result banner *@

This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.

- -@if (Model.ParsedLog?.IsValid == true) +@if (Model.UploadError != null) { -

Get help

diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png index 63eb8970..f359146c 100644 Binary files a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png and b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png differ diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png new file mode 100644 index 00000000..1de9cf47 Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png differ diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js index 8fa1c26f..d0734b02 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/index.js +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -6,7 +6,7 @@ $(document).ready(function () { pufferchick.attr("src", "Content/images/pufferchick-cool.png"); }, function () { - pufferchick.attr("src", "favicon.ico"); + pufferchick.attr("src", "Content/images/pufferchick.png"); } ); -- cgit From dc27247b28097a992694fcb7a72133ae4e56e27b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Jun 2018 01:32:52 -0400 Subject: add section anchors for links from Nexus (#547) --- src/SMAPI.Web/Views/Index/Index.cshtml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 8145d354..411448fa 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -37,7 +37,7 @@
-

Get help

+

Get help

  • Mod compatibility list
  • Get help on Discord or in the forums
  • @@ -45,7 +45,7 @@ @if (Model.BetaVersion == null) { -

    What's new in SMAPI @Model.StableVersion.Version?

    +

    What's new in SMAPI @Model.StableVersion.Version?

    @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
    @@ -53,7 +53,7 @@ } else { -

    What's new in...

    +

    What's new in...

    SMAPI @Model.StableVersion.Version?

    @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) @@ -67,7 +67,7 @@ else

    See the release notes and mod compatibility list for more info.

    } -

    Donate to support SMAPI ♥

    +

    SMAPI is an open-source project by Pathoschild. It will always be free, but donations are much appreciated to help pay for development, server hosting, domain fees, coffee, etc. @@ -98,7 +98,7 @@ else and a few anonymous users for their ongoing support; you're awesome! 🏅

    -

    For mod creators

    +

    For mod creators

    • SMAPI @Model.StableVersion.Version for developers (includes intellisense and full console output)
    • @if (Model.BetaVersion != null) -- cgit From 0043810e04239c897a61c097855ab08c382251ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Jun 2018 13:23:24 -0400 Subject: set 'large address aware' flag on SMAPI executable to fix memory issues (#431) This is safe since the vanilla game has it set too. --- docs/release-notes.md | 1 + src/SMAPI/StardewModdingAPI.csproj | 8 ++++++++ src/SMAPI/packages.config | 1 + 3 files changed, 10 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bb77762..df832c34 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. * Improved how mod warnings are shown in the console. + * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed installer error on Linux/Mac in some cases. * Fixed installer not finding some game paths. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index ab3967c5..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -27,6 +27,7 @@ 1.0.0.%2a false true + true x86 @@ -344,4 +345,11 @@ + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 3e876922..3347b037 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file -- cgit From f170b1e143c1ba483f54f3902527dea7c96468ca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 18:58:53 -0400 Subject: update compatibility list --- src/SMAPI/StardewModdingAPI.metadata.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index adf5fdd1..8ac5c734 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -168,8 +168,8 @@ "Automate": { "ID": "Pathoschild.Automate", - "Default | UpdateKey": "Nexus:1063", - "~1.9.1 | Status": "AssumeBroken" // broke in SDV 1.3 + "Default | UpdateKey": "Nexus:1063", + "~1.10-beta.7 | Status": "AssumeBroken" // broke in SDV 1.3.20 }, "Automated Doors": { @@ -685,7 +685,8 @@ "Fishing Adjust": { "ID": "shuaiz.FishingAdjustMod", - "Default | UpdateKey": "Nexus:1350" + "Default | UpdateKey": "Nexus:1350", + "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' }, "Fishing Tuner Redux": { -- cgit From 645a64cd24146edba44b89f7493ba5ea9950c3c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 19:09:00 -0400 Subject: refactor player_add command a bit (#542) --- .../Framework/Commands/Player/AddCommand.cs | 38 ++++++++++------------ 1 file changed, 17 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 5bc97225..37f4719e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -100,29 +100,25 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // find matching items SearchableItem[] matches = this.Items.GetAll().Where(p => p.NameContains(name)).ToArray(); - - // if exactly one item with the exact same name, use that item - SearchableItem[] exactNameMatches = matches.Where(p => p.NameEquivalentTo(name)).ToArray(); - if (exactNameMatches.Length == 1) - return exactNameMatches[0]; - - switch (matches.Length) + if (!matches.Any()) { - // none found - case 0: - monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error); - return null; - - // list matches - default: - string options = this.GetTableString( - data: matches, - header: new[] { "type", "name", "command" }, - getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" } - ); - monitor.Log($"There's no item with name '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info); - return null; + monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error); + return null; } + + // handle single exact match + SearchableItem[] exactMatches = matches.Where(p => p.NameEquivalentTo(name)).ToArray(); + if (exactMatches.Length == 1) + return exactMatches[0]; + + // handle ambiguous results + string options = this.GetTableString( + data: matches, + header: new[] { "type", "name", "command" }, + getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" } + ); + monitor.Log($"There's no item with name '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info); + return null; } /// Get the command description. -- cgit From d401aff3307f6e2e1641610fdd912b572d6b04c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:10:15 -0400 Subject: rewrite update checks (#551) --- docs/release-notes.md | 14 +- docs/technical-docs.md | 43 ++--- src/SMAPI.Web/Controllers/ModsApiController.cs | 180 +++++++++++++++------ .../Framework/ModRepositories/ModInfoModel.cs | 56 +++++++ src/SMAPI/Framework/IModMetadata.cs | 18 +-- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 7 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 25 +-- .../Framework/ModUpdateChecking/ModUpdateStatus.cs | 37 ----- src/SMAPI/Program.cs | 143 +++++++--------- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Framework/Clients/WebApi/ModEntryModel.cs | 30 ++++ .../Framework/Clients/WebApi/ModInfoModel.cs | 56 ------- .../Framework/Clients/WebApi/ModSeachModel.cs | 15 +- .../Clients/WebApi/ModSearchEntryModel.cs | 34 ++++ .../Framework/Clients/WebApi/WebApiClient.cs | 39 +---- 15 files changed, 366 insertions(+), 332 deletions(-) create mode 100644 src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs delete mode 100644 src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index df832c34..b3ab2481 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,8 @@ * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. + * Added update checks for optional mod files on Nexus. + * Added `player_add name` command, which lets you add items to your inventory by name instead of ID. * Improved how mod warnings are shown in the console. * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. @@ -16,6 +18,8 @@ * Fixed installer not removing some SMAPI files. * Fixed `smapi.io/install` not linking to a useful page. * Fixed `world_setseason` command not running season-change logic. + * Fixed `world_setseason` command not normalising the season value. + * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Fixed Nexus mod update alerts not showing HTTPS links. @@ -38,6 +42,7 @@ * Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map. * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. + * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. @@ -54,14 +59,9 @@ * Mods can't intercept chatbox input. * Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.) -* In console commands: - * Added `player_add name`, which lets you add items to your inventory by name instead of ID. - * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). - * Fixed `world_setseason` not normalising the season value. - * For the web UI: - * Improved log parser design to make it more intuitive. - * Improved layout on small screens. + * Redesigned log parser to make it more intuitive. + * Redesigned UI to be more mobile-friendly. * Added option to download from Nexus. * Changed log parser filters to show `DEBUG` messages by default. * Fixed log parser issue when content packs have no description. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index f4358e31..bdb731d1 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -161,7 +161,7 @@ The log parser lives at https://log.smapi.io. ### Mods API The mods API provides version info for mods hosted by Chucklefish, GitHub, or Nexus Mods. It's used by SMAPI to perform update checks. The `{version}` URL token is the version of SMAPI making the -request; it doesn't do anything currently, but lets us version breaking changes if needed. +request, and is used when needed for backwards compatibility. Each mod is identified by a repository key and unique identifier (like `nexus:541`). The following repositories are supported: @@ -173,32 +173,37 @@ key | repository `nexus` | A mod page on [Nexus Mods](https://www.nexusmods.com/stardewvalley), identified by the mod ID in the page URL. -The API accepts either `GET` or `POST` for convenience: -> ``` ->GET https://api.smapi.io/v2.0/mods?modKeys=nexus:541,chucklefish:4228 ->``` - +The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and +update keys. >``` >POST https://api.smapi.io/v2.0/mods >{ -> "ModKeys": [ "nexus:541", "chucklefish:4228" ] +> "mods": [ +> { +> "id": "Pathoschild.LookupAnything", +> "updateKeys": [ "nexus:541", "chucklefish:4250" ] +> } +> ] >} >``` -It returns a response like this: +The API will automatically aggregate versions and errors, and return a response like this. The +latest version is the main mod version (e.g. 'latest version' field on Nexus); if available and +newer, the latest optional version will be shown as the 'preview version'. >``` >{ -> "chucklefish:4228": { -> "name": "Entoarox Framework", -> "version": "1.8.0", -> "url": "https://community.playstarbound.com/resources/4228" -> }, -> "nexus:541": { -> "name": "Lookup Anything", -> "version": "1.16", -> "url": "http://www.nexusmods.com/stardewvalley/mods/541" -> } ->} +> "Pathoschild.LookupAnything": { +> "id": "Pathoschild.LookupAnything", +> "name": "Lookup Anything", +> "version": "1.18", +> "url": "https://www.nexusmods.com/stardewvalley/mods/541", +> "previewVersion": "1.19-beta", +> "previewUrl": "https://www.nexusmods.com/stardewvalley/mods/541", +> "errors": [ +> "The update key 'chucklefish:4250' matches a mod with invalid semantic version '*'." +> ] +> } +} >``` ## Development diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1ec855d5..c5a1705d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -67,70 +68,89 @@ namespace StardewModdingAPI.Web.Controllers } /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - /// Whether to allow non-semantic versions, instead of returning an error for those. - [HttpGet] - public async Task> GetAsync(string modKeys, bool allowInvalidVersions = false) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. + /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) + public async Task> PostAsync([FromBody] ModSearchModel model) { - // parse model - bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) + ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (ModSearchEntryModel mod in searchMods) { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + if (string.IsNullOrWhiteSpace(mod.ID)) continue; - } - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + // get latest versions + ModEntryModel result = new ModEntryModel { ID = mod.ID }; + IList errors = new List(); + foreach (string updateKey in mod.UpdateKeys ?? new string[0]) { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) + { + errors.Add(data.Error); + continue; + } - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - // fetch info - ModInfoModel info = await repository.GetModInfoAsync(modID); + // handle main version + if (data.Version != null) + { + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); + continue; + } + + if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + { + result.Name = data.Name; + result.Url = data.Url; + result.Version = version.ToString(); + } + } - // validate - if (info.Error == null) + // handle optional version + if (data.PreviewVersion != null) { - if (info.Version == null) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); - if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; + } + + if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + { + result.Name = result.Name ?? data.Name; + result.PreviewUrl = data.Url; + result.PreviewVersion = version.ToString(); + } } + } + + // fallback to preview if latest is invalid + if (result.Version == null && result.PreviewVersion != null) + { + result.Version = result.PreviewVersion; + result.Url = result.PreviewUrl; + result.PreviewVersion = null; + result.PreviewUrl = null; + } + + // special cases + if (mod.ID == "Pathoschild.SMAPI") + { + result.Name = "SMAPI"; + result.Url = "https://smapi.io/"; + if (result.PreviewUrl != null) + result.PreviewUrl = "https://smapi.io/"; + } - // cache & return - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); - return info; - }); + // add result + result.Errors = errors.ToArray(); + mods[mod.ID] = result; } - return result; + return mods; } @@ -158,5 +178,63 @@ namespace StardewModdingAPI.Web.Controllers modID = parts[1].Trim(); return true; } + + /// Get the mods for which the API should return data. + /// The search model. + private IEnumerable GetSearchMods(ModSearchModel model) + { + if (model == null) + yield break; + + // yield standard entries + if (model.Mods != null) + { + foreach (ModSearchEntryModel mod in model.Mods) + yield return mod; + } + + // yield mod update keys if backwards compatible + if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + { + foreach (string updateKey in model.ModKeys.Distinct()) + yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); + } + } + + /// Get the mod info for an update key. + /// The namespaced update key. + private async Task GetInfoForUpdateKeyAsync(string updateKey) + { + // parse update key + if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) + return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + + // fetch mod info + return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + ModInfoModel result = await repository.GetModInfoAsync(modID); + if (result.Error != null) + { + if (result.Version == null) + result.Error = $"The update key '{updateKey}' matches a mod with no version number."; + else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; + } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); + return result; + }); + } + + /// Get whether the API should return data in a backwards compatible way. + /// The last version for which data should be backwards compatible. + private bool ShouldBeBackwardsCompatible(string maxVersion) + { + string actualVersion = (string)this.RouteData.Values["version"]; + return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs new file mode 100644 index 00000000..ccb0699c --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// Generic metadata about a mod. + public class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's latest release number. + public string Version { get; set; } + + /// The mod's latest optional release, if newer than . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b71c8056..d3ec0035 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework { @@ -46,11 +46,9 @@ namespace StardewModdingAPI.Framework /// Whether the mod is a content pack. bool IsContentPack { get; } - /// The update status of this mod (if any). - ModUpdateStatus UpdateStatus { get; } + /// The update-check metadata for this mod (if any). + ModEntryModel UpdateCheckData { get; } - /// The preview update status of this mod (if any). - ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -78,13 +76,9 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); - /// Set the update status. - /// The mod update status. - IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); - - /// Set the preview update status. - /// The mod preview update status. - IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); + /// Set the update-check metadata for this mod. + /// The update-check metadata. + IModMetadata SetUpdateData(ModEntryModel data); /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index deb12bdc..3801fac3 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -40,9 +40,12 @@ namespace StardewModdingAPI.Framework.ModData /// Get a semantic remote version for update checks. /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) { - return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 88d2770c..02a77778 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework.ModLoading { @@ -44,11 +44,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod-provided API (if any). public object Api { get; private set; } - /// The update status of this mod (if any). - public ModUpdateStatus UpdateStatus { get; private set; } - - /// The preview update status of this mod (if any). - public ModUpdateStatus PreviewUpdateStatus { get; private set; } + /// The update-check metadata for this mod (if any). + public ModEntryModel UpdateCheckData { get; private set; } /// Whether the mod is a content pack. public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -122,19 +119,11 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the update status. - /// The mod update status. - public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) - { - this.UpdateStatus = updateStatus; - return this; - } - - /// Set the preview update status. - /// The mod preview update status. - public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) + /// Set the update-check metadata for this mod. + /// The update-check metadata. + public IModMetadata SetUpdateData(ModEntryModel data) { - this.PreviewUpdateStatus = previewUpdateStatus; + this.UpdateCheckData = data; return this; } diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs deleted file mode 100644 index efb32aef..00000000 --- a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace StardewModdingAPI.Framework.ModUpdateChecking -{ - /// Update status for a mod. - internal class ModUpdateStatus - { - /********* - ** Accessors - *********/ - /// The version that this mod can be updated to (if any). - public ISemanticVersion Version { get; } - - /// The error checking for updates of this mod (if any). - public string Error { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The version that this mod can be update to. - public ModUpdateStatus(ISemanticVersion version) - { - this.Version = version; - } - - /// Construct an instance. - /// The error checking for updates of this mod. - public ModUpdateStatus(string error) - { - this.Error = error; - } - - /// Construct an instance. - public ModUpdateStatus() - { - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2ee18a29..ccdf98ef 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -8,6 +8,7 @@ using System.Net; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Xna.Framework.Input; @@ -24,7 +25,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -592,14 +592,14 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; - if (response.Error != null) + if (latestStable == null && response.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {response.Error}"); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) { @@ -634,103 +634,72 @@ namespace StardewModdingAPI { HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - // prepare update keys - Dictionary modsByKey = - ( - from mod in mods - where - mod.Manifest?.UpdateKeys != null - && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // fetch results - this.Monitor.Log($" Checking {modsByKey.Count} mod update keys.", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary> updatesByMod = new Dictionary>(); - foreach (var result in results) + // prepare search model + List searchMods = new List(); + foreach (IModMetadata mod in mods) { - IModMetadata mod = result.Mod; - ModInfoModel remoteInfo = result.Info; - - // handle error - if (remoteInfo.Error != null) - { - if (mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - if (mod.PreviewUpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); + if (!mod.HasManifest()) continue; - } - // normalise versions - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); - bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + } - if (!validVersion && mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); - if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) - mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary results = client.GetModInfo(searchMods.ToArray()); - if (!validVersion && !validPreviewVersion) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); + // extract update alerts & errors + var updates = new List>(); + var errors = new StringBuilder(); + foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) + { + // link to update-check data + if (!mod.HasManifest() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) continue; - } - - // compare versions - bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); - bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; + mod.SetUpdateData(result); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); - if (isUpdate) + // handle errors + if (result.Errors != null && result.Errors.Any()) { - if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) - { - updatesByMod[mod] = new Tuple(remoteInfo, isPreviewUpdate); - - if (isPreviewUpdate) - mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); - else - mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); - } + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName} update error: {result.Errors[0]}" + : $" {mod.DisplayName} update errors:\n - {string.Join("\n - ", result.Errors)}" + ); } - } - // set mods to have no updates - foreach (IModMetadata mod in results.Select(item => item.Mod) - .Where(item => !updatesByMod.ContainsKey(item))) - { - mod.SetUpdateStatus(new ModUpdateStatus()); - mod.SetPreviewUpdateStatus(new ModUpdateStatus()); + // parse versions + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + ISemanticVersion latestVersion = result.Version != null + ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) + : null; + ISemanticVersion optionalVersion = result.PreviewVersion != null + ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) + : null; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) + updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); } - // output - if (updatesByMod.Any()) + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) { this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {(entry.Value.Item2 ? entry.Value.Item1.PreviewVersion : entry.Value.Item1.Version)}: {entry.Value.Item1.Url}", LogLevel.Alert); + this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updates) + { + IModMetadata mod = entry.Item1; + ISemanticVersion newVersion = entry.Item2; + string newUrl = entry.Item3; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } } else this.Monitor.Log(" All mods up to date.", LogLevel.Trace); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 916dd053..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,7 +110,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs new file mode 100644 index 00000000..0f268231 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a mod. + public class ModEntryModel + { + /********* + ** Accessors + *********/ + /// The mod's unique ID (if known). + public string ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The mod's latest release number. + public string Version { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The mod's latest optional release, if newer than . + public string PreviewVersion { get; set; } + + /// The web URL to the mod's latest optional release, if newer than . + public string PreviewUrl { get; set; } + + /// The errors that occurred while fetching update data. + public string[] Errors { get; set; } = new string[0]; + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs deleted file mode 100644 index c8e296f0..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Generic metadata about a mod. - public class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's latest release number. - public string Version { get; set; } - - /// The mod's latest optional release, if newer than . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index c0ee34ea..ffca32ca 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi @@ -10,10 +10,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// The namespaced mod keys to search. + [Obsolete] public string[] ModKeys { get; set; } - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } + /// The mods for which to find data. + public ModSearchEntryModel[] Mods { get; set; } /********* @@ -26,12 +27,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi } /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + /// The mods to search. + public ModSearchModel(ModSearchEntryModel[] mods) { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; + this.Mods = mods.ToArray(); } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs new file mode 100644 index 00000000..bca47647 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies the identifiers for a mod to match. + public class ModSearchEntryModel + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The namespaced mod update keys (if available). + public string[] UpdateKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchEntryModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The unique mod ID. + /// The namespaced mod update keys (if available). + public ModSearchEntryModel(string id, string[] updateKeys) + { + this.ID = id; + this.UpdateKeys = updateKeys ?? new string[0]; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index d94b0259..892dfeba 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using Newtonsoft.Json; @@ -31,44 +30,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Version = version; } - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) + /// Get metadata about a set of mods from the web API. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params ModSearchEntryModel[] mods) { - return this.Post>( + return this.Post>( $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) + new ModSearchModel(mods) ); } - /// Get the latest version for a mod. - /// The update keys to search. - public ISemanticVersion GetLatestVersion(string[] updateKeys) - { - if (!updateKeys.Any()) - return null; - - // fetch update results - ModInfoModel[] results = this - .GetModInfo(updateKeys) - .Values - .Where(p => p.Error == null) - .ToArray(); - if (!results.Any()) - return null; - - ISemanticVersion latest = null; - foreach (ModInfoModel result in results) - { - if (!SemanticVersion.TryParse(result.PreviewVersion ?? result.Version, out ISemanticVersion cur)) - continue; - - if (latest == null || cur.IsNewerThan(latest)) - latest = cur; - } - return latest; - } - /********* ** Private methods -- cgit From 08e9c7e7d36cbf0720c93b395e688aeb5c86b1dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:59:38 -0400 Subject: add absolute pixels to ICursorPosition, fix tile not updated if screen-relative pos didn't change (#546) --- docs/release-notes.md | 1 + src/SMAPI/Framework/CursorPosition.cs | 12 ++++++------ src/SMAPI/Framework/Input/SInputState.cs | 16 +++++++++------- src/SMAPI/ICursorPosition.cs | 3 +++ 4 files changed, 19 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b3ab2481..fa6501a3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -42,6 +42,7 @@ * Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map. * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. + * Added absolute pixels to `ICursorPosition`. * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index aaf089d3..079917f2 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -8,8 +8,8 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// The raw pixel position, not adjusted for the game zoom. - public Vector2 RawPixels { get; } + /// The pixel position relative to the top-left corner of the in-game map. + public Vector2 AbsolutePixels { get; } /// The pixel position relative to the top-left corner of the visible screen. public Vector2 ScreenPixels { get; } @@ -25,13 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. - /// The raw pixel position, not adjusted for the game zoom. + /// The pixel position relative to the top-left corner of the in-game map. /// The pixel position relative to the top-left corner of the visible screen. /// The tile position relative to the top-left corner of the map. /// The tile position that the game considers under the cursor for purposes of clicking actions. - public CursorPosition(Vector2 rawPixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + public CursorPosition(Vector2 absolutePixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) { - this.RawPixels = rawPixels; + this.AbsolutePixels = absolutePixels; this.ScreenPixels = screenPixels; this.Tile = tile; this.GrabTile = grabTile; @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework /// An object to compare with this object. public bool Equals(ICursorPosition other) { - return other != null && this.ScreenPixels == other.ScreenPixels; + return other != null && this.AbsolutePixels == other.AbsolutePixels; } } } diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 44fd0618..0228db0d 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -82,15 +82,15 @@ namespace StardewModdingAPI.Framework.Input KeyboardState realKeyboard = Keyboard.GetState(); MouseState realMouse = Mouse.GetState(); var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); - Vector2 cursorRawPixelPos = new Vector2(this.RealMouse.X, this.RealMouse.Y); + Vector2 cursorAbsolutePos = new Vector2(realMouse.X + Game1.viewport.X, realMouse.Y + Game1.viewport.Y); // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - if (this.CursorPositionImpl?.RawPixels != cursorRawPixelPos) - this.CursorPositionImpl = this.GetCursorPosition(cursorRawPixelPos); + if (this.CursorPositionImpl?.AbsolutePixels != cursorAbsolutePos) + this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos); // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); @@ -162,15 +162,17 @@ namespace StardewModdingAPI.Framework.Input ** Private methods *********/ /// Get the current cursor position. - /// The raw pixel position from the mouse state. - private CursorPosition GetCursorPosition(Vector2 rawPixelPos) + /// The current mouse state. + /// The absolute pixel position relative to the map. + private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels) { - Vector2 screenPixels = new Vector2((int)(rawPixelPos.X * (1.0 / Game1.options.zoomLevel)), (int)(rawPixelPos.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX + Vector2 rawPixels = new Vector2(mouseState.X, mouseState.Y); + Vector2 screenPixels = rawPixels * new Vector2((float)1.0 / Game1.options.zoomLevel); // derived from Game1::getMouseX Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton ? tile : Game1.player.GetGrabTile(); - return new CursorPosition(rawPixelPos, screenPixels, tile, grabTile); + return new CursorPosition(absolutePixels, screenPixels, tile, grabTile); } /// Whether input should be suppressed in the current context. diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs index 78f4fc21..21c57db0 100644 --- a/src/SMAPI/ICursorPosition.cs +++ b/src/SMAPI/ICursorPosition.cs @@ -6,6 +6,9 @@ namespace StardewModdingAPI /// Represents a cursor position in the different coordinate systems. public interface ICursorPosition : IEquatable { + /// The pixel position relative to the top-left corner of the in-game map. + Vector2 AbsolutePixels { get; } + /// The pixel position relative to the top-left corner of the visible screen. Vector2 ScreenPixels { get; } -- cgit From 94a1308eb4bb76ad1dad73978b870cc51abacdc3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 23:50:31 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index c4bafbce..a08c3091 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.16", + "Version": "2.6.0-beta.17", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 4d7589e6..fac55a0b 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.16", + "Version": "2.6.0-beta.17", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 532112ff..65ca7866 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -116,8 +116,8 @@ namespace StardewModdingAPI /// Initialise the static values. static Constants() { - Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.16"); - Constants.MinimumGameVersion = new GameVersion("1.3.17"); + Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.17"); + Constants.MinimumGameVersion = new GameVersion("1.3.20"); Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); } -- cgit From 997c65b6f46f74469fb41af81a69e07d788a1e6e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jun 2018 23:51:00 -0400 Subject: fix list_items and player_add commands not handling secret notes --- .../Framework/ItemRepository.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 833fddde..e678d057 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -83,9 +83,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework foreach (int id in Game1.bigCraftablesInformation.Keys) yield return new SearchableItem(ItemType.BigCraftable, id, new SObject(Vector2.Zero, id)); + // secret notes + foreach (int id in Game1.content.Load>("Data\\SecretNotes").Keys) + { + SObject note = new SObject(79, 1); + note.name = $"{note.name} #{id}"; + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, note); + } + // objects foreach (int id in Game1.objectInformation.Keys) { + if (id == 79) + continue; // secret note handled above if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange) continue; // handled separated @@ -103,7 +113,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; wine.preserve.Value = SObject.PreserveType.Wine; wine.preservedParentSheetIndex.Value = item.parentSheetIndex; - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, wine); + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, wine); // jelly SObject jelly = new SObject(344, 1) @@ -113,7 +123,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; jelly.preserve.Value = SObject.PreserveType.Jelly; jelly.preservedParentSheetIndex.Value = item.parentSheetIndex; - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, jelly); + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, jelly); } // vegetable products @@ -127,7 +137,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; juice.preserve.Value = SObject.PreserveType.Juice; juice.preservedParentSheetIndex.Value = item.parentSheetIndex; - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, juice); + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, juice); // pickled SObject pickled = new SObject(342, 1) @@ -137,7 +147,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework }; pickled.preserve.Value = SObject.PreserveType.Pickle; pickled.preservedParentSheetIndex.Value = item.parentSheetIndex; - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, pickled); + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, pickled); } // flower honey -- cgit From e149f20583b90c37964c48b8f13777493611aecf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jun 2018 16:50:15 -0400 Subject: remove seasonal tilesheet patch (#552) This is no longer needed (the changes were added to the game in SDV 1.3.19), and caused an issue since it left out the tilesheet reloading. --- src/SMAPI/Framework/Patching/GameLocationPatch.cs | 60 ----------------------- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 - 3 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 src/SMAPI/Framework/Patching/GameLocationPatch.cs (limited to 'src') diff --git a/src/SMAPI/Framework/Patching/GameLocationPatch.cs b/src/SMAPI/Framework/Patching/GameLocationPatch.cs deleted file mode 100644 index c0216b8f..00000000 --- a/src/SMAPI/Framework/Patching/GameLocationPatch.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using Harmony; -using StardewValley; -using xTile.Tiles; - -namespace StardewModdingAPI.Framework.Patching -{ - /// A Harmony patch for the method. - internal class GameLocationPatch : IHarmonyPatch - { - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(GameLocation)}.{nameof(GameLocation.updateSeasonalTileSheets)}"; - - - /********* - ** Public methods - *********/ - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - MethodInfo method = AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GameLocationPatch.Prefix)); - - harmony.Patch(method, new HarmonyMethod(prefix), null); - } - - - /********* - ** Private methods - *********/ - /// An implementation of which correctly handles custom map tilesheets. - /// The location instance being patched. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument name is defined by Harmony.")] - private static bool Prefix(GameLocation __instance) - { - if (!__instance.IsOutdoors || __instance.Name.Equals("Desert")) - return false; - foreach (TileSheet tilesheet in __instance.Map.TileSheets) - { - string imageSource = tilesheet.ImageSource; - string imageFile = Path.GetFileName(imageSource); - if (imageFile.StartsWith("spring_") || imageFile.StartsWith("summer_") || imageFile.StartsWith("fall_") || imageFile.StartsWith("winter_")) - { - string imageDir = Path.GetDirectoryName(imageSource); - if (string.IsNullOrWhiteSpace(imageDir)) - imageDir = "Maps"; - tilesheet.ImageSource = Path.Combine(imageDir, Game1.currentSeason + "_" + imageFile.Split('_')[1]); - } - } - - return false; - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ccdf98ef..53f3749a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -168,7 +168,7 @@ namespace StardewModdingAPI // apply game patches new GamePatcher(this.Monitor).Apply( - new GameLocationPatch() + // new GameLocationPatch() ); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index fcd54c34..bd9e2422 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -290,7 +290,6 @@ - -- cgit From a2520024b17e3073d2fffbb0aa84e5f30801f36c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jun 2018 20:25:26 -0400 Subject: drop support for mods without IDs in metadata file --- src/SMAPI/Framework/ModData/ModDatabase.cs | 74 +------------ src/SMAPI/StardewModdingAPI.metadata.json | 169 +---------------------------- 2 files changed, 9 insertions(+), 234 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs index 3fd68440..62f37d9b 100644 --- a/src/SMAPI/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI/Framework/ModData/ModDatabase.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using Newtonsoft.Json; namespace StardewModdingAPI.Framework.ModData { @@ -40,7 +38,7 @@ namespace StardewModdingAPI.Framework.ModData public ParsedModDataRecord GetParsed(IManifest manifest) { // get raw record - if (!this.TryGetRaw(manifest, out string displayName, out ModDataRecord record)) + if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record)) return null; // parse fields @@ -111,27 +109,7 @@ namespace StardewModdingAPI.Framework.ModData /// The raw mod record. private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) { - foreach (var entry in this.Records) - { - if (entry.Value.ID != null && entry.Value.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - { - displayName = entry.Key; - record = entry.Value; - return true; - } - } - - displayName = null; - record = null; - return false; - } - /// Get a raw data record. - /// The mod manifest whose fields to match. - /// The mod's default display name. - /// The raw mod record. - private bool TryGetRaw(IManifest manifest, out string displayName, out ModDataRecord record) - { - if (manifest != null) + if (!string.IsNullOrWhiteSpace(id)) { foreach (var entry in this.Records) { @@ -139,7 +117,7 @@ namespace StardewModdingAPI.Framework.ModData record = entry.Value; // try main ID - if (record.ID != null && record.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) return true; // try former IDs @@ -147,26 +125,7 @@ namespace StardewModdingAPI.Framework.ModData { foreach (string part in record.FormerIDs.Split('|')) { - // packed field snapshot - if (part.StartsWith("{")) - { - FieldSnapshot snapshot = JsonConvert.DeserializeObject(part); - bool isMatch = - (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase)) - && ( - snapshot.Author == null - || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields != null && manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); - - if (isMatch) - return true; - } - - // plain ID - else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) return true; } } @@ -177,30 +136,5 @@ namespace StardewModdingAPI.Framework.ModData record = null; return false; } - - - /********* - ** Private models - *********/ - /// A unique set of fields which identifies the mod. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] - private class FieldSnapshot - { - /********* - ** Accessors - *********/ - /// The unique mod ID (or null to ignore it). - public string ID { get; set; } - - /// The entry DLL (or null to ignore it). - public string EntryDll { get; set; } - - /// The mod name (or null to ignore it). - public string Name { get; set; } - - /// The author name (or null to ignore it). - public string Author { get; set; } - } } } diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 8ac5c734..61a5befb 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -11,14 +11,8 @@ * - ID: the mod's latest unique ID (if any). * * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching - * other fields if no ID was specified. This doesn't include the latest ID, if any. - * Format rules: - * 1. If the mod's ID changed over time, multiple variants can be separated by the '|' - * character. - * 2. Each variant can take one of two forms: - * - A simple string matching the mod's UniqueID value. - * - A JSON structure containing any of four manifest fields (ID, Name, Author, and - * EntryDll) to match. + * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple + * variants can be separated with '|'. * * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions * during update checks. For example, if the API returns version '1.1-1078' where '1078' is @@ -106,7 +100,6 @@ "Almighty Farming Tool": { "ID": "439", - "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 "MapRemoteVersions": { "1.21": "1.2.1" }, "Default | UpdateKey": "Nexus:439" }, @@ -125,7 +118,6 @@ "Animal Sitter": { "ID": "jwdred.AnimalSitter", - "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 "Default | UpdateKey": "Nexus:581", "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -135,11 +127,6 @@ "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals }, - "Ashley Mod": { - "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "A Tapper's Dream": { "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", "Default | UpdateKey": "Nexus:260" @@ -180,7 +167,6 @@ "AutoSpeed": { "ID": "Omegasis.AutoSpeed", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:443" // added in 1.4.1 }, @@ -224,7 +210,6 @@ "Billboard Anywhere": { "ID": "Omegasis.BillboardAnywhere", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:492" // added in 1.4.1 }, @@ -242,13 +227,11 @@ "Build Endurance": { "ID": "Omegasis.BuildEndurance", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:445" // added in 1.4.1 }, "Build Health": { "ID": "Omegasis.BuildHealth", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:446" // added in 1.4.1 }, @@ -293,7 +276,6 @@ "Chest Pooling": { "ID": "mralbobo.ChestPooling", - "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling" }, @@ -304,12 +286,6 @@ "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3 }, - "Choose Baby Gender": { - "FormerIDs": "{EntryDll: 'ChooseBabyGender.dll'}", - "Default | UpdateKey": "Nexus:590", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "CJB Automation": { "ID": "CJBAutomation", "Default | UpdateKey": "Nexus:211", @@ -411,7 +387,6 @@ "CrabNet": { "ID": "jwdred.CrabNet", - "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 "Default | UpdateKey": "Nexus:584" }, @@ -617,7 +592,6 @@ "Extended Minecart": { "ID": "Entoarox.ExtendedMinecart", - "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) }, @@ -628,21 +602,10 @@ "Fall 28 Snow Day": { "ID": "Omegasis.Fall28SnowDay", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:486", // added in 1.4.1 "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything }, - "Farm Automation: Barn Door Automation": { - "FormerIDs": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Automation: Item Collector": { - "FormerIDs": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - "Farm Automation Unofficial: Item Collector": { "ID": "Maddy99.FarmAutomation.ItemCollector", "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 @@ -654,12 +617,6 @@ "Default | UpdateKey": "Nexus:130" }, - "Farm Resource Generator": { - "FormerIDs": "{EntryDll: 'FarmResourceGenerator.dll'}", - "Default | UpdateKey": "Nexus:647", - "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "Fast Animations": { "ID": "Pathoschild.FastAnimations", "Default | UpdateKey": "Nexus:1089", @@ -673,16 +630,10 @@ "Faster Paths": { "ID": "Entoarox.FasterPaths", - "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander + "FormerIDs": "615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.3 "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) }, - "Faster Run": { - "ID": "KathrynHazuka.FasterRun", - "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update - "Default | UpdateKey": "Nexus:733" // added in 1.1.1-pathoschild-update - }, - "Fishing Adjust": { "ID": "shuaiz.FishingAdjustMod", "Default | UpdateKey": "Nexus:1350", @@ -699,13 +650,6 @@ "Default | UpdateKey": "Nexus:1941" }, - "FlorenceMod": { - "FormerIDs": "{EntryDll: 'FlorenceMod.dll'}", - "MapLocalVersions": { "1.0.1": "1.1" }, - "Default | UpdateKey": "Nexus:591", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "Flower Color Picker": { "ID": "spacechase0.FlowerColorPicker", "Default | UpdateKey": "Nexus:1229" @@ -719,7 +663,6 @@ "Furniture Anywhere": { "ID": "Entoarox.FurnitureAnywhere", - "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) }, @@ -730,7 +673,6 @@ "Gate Opener": { "ID": "mralbobo.GateOpener", - "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener" }, @@ -746,7 +688,6 @@ "Get Dressed": { "ID": "Advize.GetDressed", - "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 "Default | UpdateKey": "Nexus:331" }, @@ -773,15 +714,9 @@ "Happy Birthday (Omegasis)": { "ID": "Omegasis.HappyBirthday", - "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork "Default | UpdateKey": "Nexus:520" // added in 1.4.1 }, - "Happy Birthday (Oxyligen fork)": { - "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:1064" // missing key reported: https://www.nexusmods.com/stardewvalley/mods/1064?tab=bugs - }, - "Hardcore Mines": { "ID": "kibbe.hardcore_mines", "Default | UpdateKey": "Nexus:1674" @@ -859,12 +794,6 @@ "Default | UpdateKey": "Nexus:1184" }, - "Jiggly Junimo Bundles": { - "ID": "Greger.JigglyJunimoBundles", - "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update - "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update - }, - "Json Assets": { "ID": "spacechase0.JsonAssets", "Default | UpdateKey": "Nexus:1720" @@ -920,7 +849,6 @@ "Loved Labels": { "ID": "Advize.LovedLabels", - "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 "Default | UpdateKey": "Nexus:279" }, @@ -942,7 +870,6 @@ "MailOrderPigs": { "ID": "jwdred.MailOrderPigs", - "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 "Default | UpdateKey": "Nexus:632" }, @@ -1005,7 +932,6 @@ "More Rain": { "ID": "Omegasis.MoreRain", - "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:441", // added in 1.5.1 "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -1030,7 +956,6 @@ "Museum Rearranger": { "ID": "Omegasis.MuseumRearranger", - "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:428" // added in 1.4.1 }, @@ -1047,7 +972,6 @@ "Night Owl": { "ID": "Omegasis.NightOwl", - "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest "Default | UpdateKey": "Nexus:433" // added in 1.4.1 }, @@ -1101,12 +1025,6 @@ "Default | UpdateKey": "Nexus:239" }, - "NPC Speak": { - "FormerIDs": "{EntryDll: 'NpcEcho.dll'}", - "Default | UpdateKey": "Nexus:694", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "Object Time Left": { "ID": "spacechase0.ObjectTimeLeft", "Default | UpdateKey": "Nexus:1315" @@ -1135,7 +1053,6 @@ "PelicanFiber": { "ID": "jwdred.PelicanFiber", - "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 "Default | UpdateKey": "Nexus:631" }, @@ -1154,12 +1071,6 @@ "Default | UpdateKey": "Nexus:1778" }, - "Persival's BundleMod": { - "FormerIDs": "{EntryDll: 'BundleMod.dll'}", - "Default | UpdateKey": "Nexus:438", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 - }, - "Plant on Grass": { "ID": "Demiacle.PlantOnGrass", "Default | UpdateKey": "Nexus:1026" @@ -1172,7 +1083,6 @@ "Point-and-Plant": { "ID": "jwdred.PointAndPlant", - "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 "Default | UpdateKey": "Nexus:572", "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated }, @@ -1189,7 +1099,6 @@ "Prairie King Made Easy": { "ID": "Mucchan.PrairieKingMadeEasy", - "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 "Default | UpdateKey": "Chucklefish:3594", "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 }, @@ -1204,11 +1113,6 @@ "Default | UpdateKey": "Nexus:1239" }, - "Rain Randomizer": { - "FormerIDs": "{EntryDll: 'RainRandomizer.dll'}", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "Recatch Legendary Fish": { "ID": "cantorsdust.RecatchLegendaryFish", "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 @@ -1245,7 +1149,6 @@ "Replanter": { "ID": "jwdred.Replanter", - "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 "Default | UpdateKey": "Nexus:589" }, @@ -1271,12 +1174,6 @@ "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 }, - "Rise and Shine": { - "ID": "Yoshify.RiseAndShine", - "FormerIDs": "{EntryDll: 'RiseAndShine.dll'}", // changed in 1.1.1-whisk-update - "Default | UpdateKey": "Nexus:3" - }, - "Rope Bridge": { "ID": "RopeBridge", "Default | UpdateKey": "Nexus:824" @@ -1296,14 +1193,12 @@ "Save Anywhere": { "ID": "Omegasis.SaveAnywhere", - "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl "Default | UpdateKey": "Nexus:444", // added in 2.6.1 "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest }, "Save Backup": { "ID": "Omegasis.SaveBackup", - "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:435", // added in 1.3.1 "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -1376,7 +1271,7 @@ "Shop Expander": { "ID": "Entoarox.ShopExpander", - "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + "FormerIDs": "EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths "MapRemoteVersions": { "1.6.0b": "1.6.0" }, "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature) }, @@ -1405,7 +1300,6 @@ "Simple Sprinklers": { "ID": "tZed.SimpleSprinkler", - "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 "Default | UpdateKey": "Nexus:76" }, @@ -1507,24 +1401,11 @@ "Default | UpdateKey": "Chucklefish:4201" }, - "Sprinting Mod": { - "FormerIDs": "{EntryDll: 'SprintingMod.dll', Author: 'Patrick'}", - "MapLocalVersions": { "2.0": "3.0" }, // not updated in manifest - "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", - "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - "StackSplitX": { "ID": "tstaples.StackSplitX", - "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 "Default | UpdateKey": "Nexus:798" }, - "StaminaRegen": { - "FormerIDs": "{EntryDll: 'StaminaRegen.dll'}", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "Stardew Config Menu": { "ID": "Juice805.StardewConfigMenu", "Default | UpdateKey": "Nexus:1312" @@ -1548,7 +1429,6 @@ "Stardew Symphony": { "ID": "Omegasis.StardewSymphony", - "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:425" // added in 1.4.1 }, @@ -1576,13 +1456,6 @@ "Default | UpdateKey": "Chucklefish:4314" }, - "Stone Bridge Over Pond (PondWithBridge)": { - "FormerIDs": "{EntryDll: 'PondWithBridge.dll'}", - "MapLocalVersions": { "0.0": "1.0" }, - "Default | UpdateKey": "Nexus:316", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "Stumps to Hardwood Stumps": { "ID": "StumpsToHardwoodStumps", "Default | UpdateKey": "Nexus:691" @@ -1605,11 +1478,6 @@ "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 }, - "Tainted Cellar": { - "ID": "TaintedCellar", - "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}" // changed in 1.1 - }, - "Tapper Ready": { "ID": "skunkkk.TapperReady", "Default | UpdateKey": "Nexus:1219" @@ -1651,7 +1519,7 @@ "TimeSpeed": { "ID": "cantorsdust.TimeSpeed", - "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis + "FormerIDs": "community.TimeSpeed", // changed in 2.3.3 "Default | UpdateKey": "Nexus:169" }, @@ -1705,27 +1573,11 @@ "Default | UpdateKey": "Nexus:943" }, - "WakeUp": { - "FormerIDs": "{EntryDll: 'WakeUp.dll'}", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Wallpaper Fix": { - "FormerIDs": "{EntryDll: 'WallpaperFix.dll'}", - "Default | UpdateKey": "Chucklefish:4211", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - "WarpAnimals": { "ID": "Symen.WarpAnimals", "Default | UpdateKey": "Nexus:1400" }, - "Weather Controller": { - "FormerIDs": "{EntryDll: 'WeatherController.dll'}", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - "What Farm Cave / WhatAMush": { "ID": "WhatAMush", "Default | UpdateKey": "Nexus:1097" @@ -1741,16 +1593,6 @@ "Default | UpdateKey": "Nexus:1601" }, - "Wonderful Farm Life": { - "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}" - }, - - "XmlSerializerRetool": { - "FormerIDs": "{EntryDll: 'XmlSerializerRetool.dll'}", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - "Xnb Loader": { "ID": "Entoarox.XnbLoader", "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature) @@ -1791,7 +1633,6 @@ "Zoryn's Health Bars": { "ID": "Zoryn.HealthBars", - "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, -- cgit From af92f2dc13c9a259c25a15d9bdfb80d73a0bec83 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 15:08:58 -0400 Subject: add more verbose logs to simplify troubleshooting --- src/SMAPI/Framework/SGame.cs | 18 +++++++++++++----- .../Framework/Clients/WebApi/ModEntryModel.cs | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 984c1f57..abe7bbc5 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -382,7 +382,7 @@ namespace StardewModdingAPI.Framework if (this.Watchers.WindowSizeWatcher.IsChanged) { if (this.VerboseLogging) - this.Monitor.Log($"Context: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); this.Events.Graphics_Resize.Raise(); this.Watchers.WindowSizeWatcher.Reset(); } @@ -415,6 +415,8 @@ namespace StardewModdingAPI.Framework int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; this.Watchers.MouseWheelScrollWatcher.Reset(); + if (this.VerboseLogging) + this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now)); } @@ -426,6 +428,9 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { + if (this.VerboseLogging) + this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState)); this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); @@ -445,6 +450,9 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { + if (this.VerboseLogging) + this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState)); this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); @@ -605,7 +613,7 @@ namespace StardewModdingAPI.Framework this.Watchers.TimeWatcher.Reset(); if (this.VerboseLogging) - this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace); + this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } @@ -629,7 +637,7 @@ namespace StardewModdingAPI.Framework foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) { if (this.VerboseLogging) - this.Monitor.Log($"Context: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); } @@ -638,7 +646,7 @@ namespace StardewModdingAPI.Framework if (changedItems.Any()) { if (this.VerboseLogging) - this.Monitor.Log("Context: player inventory changed.", LogLevel.Trace); + this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); } @@ -1113,7 +1121,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } - label_140: + label_140: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 0f268231..e4ab168e 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod name. public string Name { get; set; } - /// The mod's latest release number. + /// The mod's latest version number. public string Version { get; set; } /// The mod's web URL. -- cgit From 9bb268a08288820d0acb5a1f089515a31f7dd030 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 16:03:23 -0400 Subject: fix input API not exposed through helper --- src/SMAPI/IModHelper.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 68c2f1c4..d7b8c986 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI /// An API for loading content assets. IContentHelper Content { get; } + /// An API for checking and changing input state. + IInputHelper Input { get; } + /// Simplifies access to private game code. IReflectionHelper Reflection { get; } -- cgit From 31e1960b4f36256b570fbba6a22ac3a6cfb41d92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 18:34:32 -0400 Subject: update unit test packages --- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 15 +++++++++------ src/SMAPI.Tests/app.config | 6 +++++- src/SMAPI.Tests/packages.config | 9 +++++---- 4 files changed, 20 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index b5630314..956e6cf3 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 04c8d12f..9a203b3b 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -32,10 +32,10 @@ - ..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll + ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll - ..\packages\Moq.4.8.2\lib\net45\Moq.dll + ..\packages\Moq.4.8.3\lib\net45\Moq.dll ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll @@ -45,11 +45,14 @@ - - ..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.1\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll - - ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll + + ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + + ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll diff --git a/src/SMAPI.Tests/app.config b/src/SMAPI.Tests/app.config index 7d8c9227..673b91c4 100644 --- a/src/SMAPI.Tests/app.config +++ b/src/SMAPI.Tests/app.config @@ -4,7 +4,11 @@ - + + + + + diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config index bad26cfa..7c3ec9f1 100644 --- a/src/SMAPI.Tests/packages.config +++ b/src/SMAPI.Tests/packages.config @@ -1,9 +1,10 @@  - - + + - - + + + \ No newline at end of file -- cgit From 9d33aaf832652a2183af863db6103bf6f8e5a682 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 18:53:33 -0400 Subject: update web/toolkit packages --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 14 +++++++------- .../StardewModdingAPI.Toolkit.csproj | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 9210565a..a409e6eb 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 6688a2a1..dda7c17c 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -10,7 +10,7 @@ - + -- cgit From 1c10e54d050b0b3c869992848b912962da0b8304 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 18:55:04 -0400 Subject: update analyzer packages --- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- .../StardewModdingAPI.ModBuildConfig.Analyzer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index 956e6cf3..c6241ecb 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj index c32343e3..9d3f6d5b 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj @@ -12,7 +12,7 @@ - + -- cgit From ebc603844a3931bedbd512761ba8f152a4f5a09c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 19:49:34 -0400 Subject: update to Mono.Cecil 0.10 --- docs/release-notes.md | 1 + .../ModLoading/AssemblyDefinitionResolver.cs | 9 ------ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 37 +++++++++++++++------- .../Framework/ModLoading/PlatformAssemblyMap.cs | 12 +++++-- .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 2 +- .../Rewriters/FieldToPropertyRewriter.cs | 2 +- .../ModLoading/Rewriters/MethodParentRewriter.cs | 2 +- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 2 +- src/SMAPI/Program.cs | 4 +-- src/SMAPI/StardewModdingAPI.csproj | 12 +++---- src/SMAPI/packages.config | 2 +- 11 files changed, 49 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index fa6501a3..5a9e4e92 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -83,6 +83,7 @@ * uses net field events where available; * lays groundwork for tracking events for multiple players. * Split mod DB out of `StardewModdingAPI.config.json` into its own file. + * Updated to Mono.Cecil 0.10. ## 2.5.5 * For players: diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index d85a9a28..33cd6ebd 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -36,15 +36,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly reader parameters. public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); - /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 2fb2aba7..d41774a9 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -12,7 +12,7 @@ using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading { /// Preprocesses and loads mod assemblies. - internal class AssemblyLoader + internal class AssemblyLoader : IDisposable { /********* ** Properties @@ -20,9 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// Whether to enable developer mode logging. - private readonly bool IsDeveloperMode; - /// Metadata for mapping assemblies to the current platform. private readonly PlatformAssemblyMap AssemblyMap; @@ -32,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// A minimal assembly definition resolver which resolves references to known loaded assemblies. private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver; + /// The objects to dispose as part of this instance. + private readonly HashSet Disposables = new HashSet(); + /********* ** Public methods @@ -39,13 +39,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The current game platform. /// Encapsulates monitoring and logging. - /// Whether to enable developer mode logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) + public AssemblyLoader(Platform targetPlatform, IMonitor monitor) { this.Monitor = monitor; - this.IsDeveloperMode = isDeveloperMode; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - this.AssemblyDefinitionResolver = new AssemblyDefinitionResolver(); + this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary(); @@ -144,10 +142,26 @@ namespace StardewModdingAPI.Framework.ModLoading .FirstOrDefault(p => p.GetName().Name == shortName); } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + foreach (IDisposable instance in this.Disposables) + instance.Dispose(); + } + /********* ** Private methods *********/ + /// Track an object for disposal as part of the assembly loader. + /// The instance type. + /// The disposable instance. + private T TrackForDisposal(T instance) where T : IDisposable + { + this.Disposables.Add(instance); + return instance; + } + /**** ** Assembly parsing ****/ @@ -166,9 +180,8 @@ namespace StardewModdingAPI.Framework.ModLoading // read assembly byte[] assemblyBytes = File.ReadAllBytes(file.FullName); - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); + Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes)); + AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true })); // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index f0a28b4a..01460dce 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -7,7 +8,7 @@ using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.ModLoading { /// Metadata for mapping assemblies to the current . - internal class PlatformAssemblyMap + internal class PlatformAssemblyMap : IDisposable { /********* ** Accessors @@ -50,7 +51,14 @@ namespace StardewModdingAPI.Framework.ModLoading // cache assembly metadata this.Targets = targetAssemblies; this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName, new ReaderParameters { InMemory = true })); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + foreach (ModuleDefinition module in this.TargetModules.Values) + module.Dispose(); } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 63358b39..806a074f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters if (!this.IsMatch(instruction)) return InstructionHandleResult.None; - FieldReference newRef = module.Import(this.ToField); + FieldReference newRef = module.ImportReference(this.ToField); cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); return InstructionHandleResult.Rewritten; } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index b1fa377a..e6ede9e3 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return InstructionHandleResult.None; string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); + MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); return InstructionHandleResult.Rewritten; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 974fcf4c..99bd9125 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return InstructionHandleResult.None; MethodReference methodRef = (MethodReference)instruction.Operand; - methodRef.DeclaringType = module.Import(this.ToType); + methodRef.DeclaringType = module.ImportReference(this.ToType); return InstructionHandleResult.Rewritten; } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 74f2fcdd..5c7db902 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // root type if (type.FullName == this.FromTypeName) - return module.Import(this.ToType); + return module.ImportReference(this.ToType); // generic arguments if (type is GenericInstanceType genericType) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 53f3749a..7b5176a0 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -686,7 +686,7 @@ namespace StardewModdingAPI // show update errors if (errors.Length != 0) - this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors, LogLevel.Trace); // show update alerts if (updates.Any()) @@ -796,7 +796,7 @@ namespace StardewModdingAPI ); // get assembly loaders - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.DeveloperMode); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index bd9e2422..2e3ba1cd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -58,16 +58,16 @@ False ..\lib\0Harmony.dll - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + + ..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.dll True - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + + ..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.Mdb.dll True - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + + ..\packages\Mono.Cecil.0.10.0\lib\net40\Mono.Cecil.Pdb.dll True diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 3347b037..84c6bed0 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file -- cgit From 71efadf2322a622bc5a74614b1575d2680a84165 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:26:42 -0400 Subject: add project for toolkit interfaces visible to SMAPI mods (#532) --- build/common.targets | 8 +- build/prepare-install-package.targets | 4 + build/prepare-nuget-package.targets | 2 + src/SMAPI.Installer/InteractiveInstaller.cs | 2 + src/SMAPI.ModBuildConfig/build/smapi.targets | 5 ++ src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 4 + src/SMAPI.sln | 89 ++++++++++++---------- src/SMAPI/StardewModdingAPI.csproj | 4 + .../Properties/AssemblyInfo.cs | 4 + ...StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 19 +++++ .../Properties/AssemblyInfo.cs | 3 - .../StardewModdingAPI.Toolkit.csproj | 8 ++ 12 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 0b8f278f..5b6511f8 100644 --- a/build/common.targets +++ b/build/common.targets @@ -25,7 +25,7 @@ - + @@ -113,10 +113,14 @@ - + + + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 5e00d663..33a92f71 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,6 +41,8 @@ + + @@ -56,6 +58,8 @@ + + diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 5dbc5508..11d1845e 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -13,6 +13,8 @@ + + diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index f9239604..1221f659 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -96,6 +96,8 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.metadata.json"); yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d33a9637..0869be66 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -92,6 +92,11 @@ false true + + $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + false + true + $(GamePath)\xTile.dll false diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 9a203b3b..b2d98d23 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -76,6 +76,10 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} StardewModdingAPI.Toolkit diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 0eb42cce..005537de 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.Save EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit.CoreInterfaces", "StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj", "{D5CFD923-37F1-4BC3-9BE8-E506E202AC28}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 @@ -70,50 +72,53 @@ Global SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Default = Debug|Default - Release|Default = Release|Default + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.Build.0 = Release|x86 - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.Build.0 = Release|Any CPU - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.ActiveCfg = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.Build.0 = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.ActiveCfg = Release|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.Build.0 = Release|x86 - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.ActiveCfg = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.Build.0 = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.ActiveCfg = Release|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.Build.0 = Release|Any CPU + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.Build.0 = Release|x86 + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.Build.0 = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.ActiveCfg = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.Build.0 = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.ActiveCfg = Release|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.Build.0 = Release|x86 + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.Build.0 = Release|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2e3ba1cd..a429d204 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -336,6 +336,10 @@ + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} StardewModdingAPI.Toolkit diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a29ba6cf --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("SMAPI.Toolkit.CoreInterfaces")] +[assembly: AssemblyDescription("Provides toolkit interfaces which are available to SMAPI mods.")] diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj new file mode 100644 index 00000000..e003122e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -0,0 +1,19 @@ + + + + net4.5;netstandard2.0 + false + + + + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces + StardewModdingAPI + + + + + + + + + diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 22dcdd96..1bb19e8c 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -3,8 +3,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] -[assembly: AssemblyProduct("SMAPI Toolkit")] -[assembly: AssemblyVersion("0.1.0")] -[assembly: AssemblyFileVersion("0.1.0")] [assembly: InternalsVisibleTo("StardewModdingAPI")] [assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index dda7c17c..904f6786 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -9,12 +9,20 @@ ..\..\bin\$(Configuration)\SMAPI.Toolkit + + + + + + + + -- cgit From 316242eeb2b6b6e711ab98f64c147a59c1d0aab8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:29:10 -0400 Subject: merge ISemanticVersion interfaces into new project (#532) --- .../StardewModdingAPI.Mods.SaveBackup.csproj | 4 + src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI/Constants.cs | 16 +--- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 11 ++- src/SMAPI/ISemanticVersion.cs | 62 -------------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 + src/SMAPI/Program.cs | 2 +- src/SMAPI/SemanticVersion.cs | 31 ++++--- src/SMAPI/StardewModdingAPI.csproj | 1 - .../ISemanticVersion.cs | 62 ++++++++++++++ src/StardewModdingAPI.Toolkit/ISemanticVersion.cs | 46 ----------- src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 95 ++++++++++++++-------- 12 files changed, 158 insertions(+), 177 deletions(-) delete mode 100644 src/SMAPI/ISemanticVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs delete mode 100644 src/StardewModdingAPI.Toolkit/ISemanticVersion.cs (limited to 'src') diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index 44fff536..afba15a1 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -51,6 +51,10 @@ StardewModdingAPI False + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index f4ade7de..09bad112 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; - bool isBeta = version.Tag != null; + bool isBeta = version.IsPrerelease(); bool isForDevs = match.Groups["forDevs"].Success; yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 65ca7866..df70d603 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.17"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -70,9 +70,6 @@ namespace StardewModdingAPI /**** ** Internal ****/ - /// SMAPI's current semantic version as a mod toolkit version. - internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } - /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; @@ -113,15 +110,6 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ - /// Initialise the static values. - static Constants() - { - Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.17"); - Constants.MinimumGameVersion = new GameVersion("1.3.20"); - - Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); - } - /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 5c7db902..de9c439a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The new type to reference. private readonly Type ToType; + /// A lambda which indicates whether a matching type reference should be rewritten. + private readonly Func ShouldRewrite; + /********* ** Public methods @@ -24,11 +27,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - public TypeReferenceRewriter(string fromTypeFullName, Type toType) + /// A lambda which indicates whether a matching type reference should be rewritten. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldRewrite = null) : base(fromTypeFullName, InstructionHandleResult.None) { this.FromTypeName = fromTypeFullName; this.ToType = toType; + this.ShouldRewrite = shouldRewrite ?? (type => true); } /// Perform the predefined logic for a method if applicable. @@ -135,7 +140,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // root type if (type.FullName == this.FromTypeName) + { + if (!this.ShouldRewrite(type)) + return type; return module.ImportReference(this.ToType); + } // generic arguments if (type is GenericInstanceType genericType) diff --git a/src/SMAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs deleted file mode 100644 index 961ef777..00000000 --- a/src/SMAPI/ISemanticVersion.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - int PatchVersion { get; } - - /// An optional build tag. - string Build { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsOlderThan(string other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsNewerThan(string other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - bool IsBetween(string min, string max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 1063ae71..543d44a7 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), + // rewrite for SMAPI 2.6 + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // moved to SMAPI.Toolkit.CoreInterfaces + // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7b5176a0..a51d6380 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -585,7 +585,7 @@ namespace StardewModdingAPI #if !SMAPI_FOR_WINDOWS url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac #endif - WebApiClient client = new WebApiClient(url, Constants.ApiVersionForToolkit); + WebApiClient client = new WebApiClient(url, Constants.ApiVersion); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index c4dd1912..587ff286 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -10,23 +10,23 @@ namespace StardewModdingAPI ** Properties *********/ /// The underlying semantic version implementation. - private readonly Toolkit.ISemanticVersion Version; + private readonly ISemanticVersion Version; /********* ** Accessors *********/ /// The major version incremented for major API changes. - public int MajorVersion => this.Version.Major; + public int MajorVersion => this.Version.MajorVersion; /// The minor version incremented for backwards-compatible changes. - public int MinorVersion => this.Version.Minor; + public int MinorVersion => this.Version.MinorVersion; /// The patch version for backwards-compatible bug fixes. - public int PatchVersion => this.Version.Patch; + public int PatchVersion => this.Version.PatchVersion; /// An optional build tag. - public string Build => this.Version.Tag; + public string Build => this.Version.Build; /********* @@ -56,7 +56,7 @@ namespace StardewModdingAPI /// Construct an instance. /// The underlying semantic version implementation. - internal SemanticVersion(Toolkit.ISemanticVersion version) + internal SemanticVersion(ISemanticVersion version) { this.Version = version; } @@ -64,7 +64,7 @@ namespace StardewModdingAPI /// Whether this is a pre-release version. public bool IsPrerelease() { - return !string.IsNullOrWhiteSpace(this.Build); + return this.Version.IsPrerelease(); } /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. @@ -73,15 +73,14 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - Toolkit.ISemanticVersion toolkitOther = new Toolkit.SemanticVersion(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); - return this.Version.CompareTo(toolkitOther); + return this.Version.CompareTo(other); } /// Get whether this version is older than the specified version. /// The version to compare with this instance. public bool IsOlderThan(ISemanticVersion other) { - return this.CompareTo(other) < 0; + return this.Version.IsOlderThan(other); } /// Get whether this version is older than the specified version. @@ -89,14 +88,14 @@ namespace StardewModdingAPI /// The specified version is not a valid semantic version. public bool IsOlderThan(string other) { - return this.IsOlderThan(new SemanticVersion(other)); + return this.Version.IsOlderThan(other); } /// Get whether this version is newer than the specified version. /// The version to compare with this instance. public bool IsNewerThan(ISemanticVersion other) { - return this.CompareTo(other) > 0; + return this.Version.IsNewerThan(other); } /// Get whether this version is newer than the specified version. @@ -104,7 +103,7 @@ namespace StardewModdingAPI /// The specified version is not a valid semantic version. public bool IsNewerThan(string other) { - return this.IsNewerThan(new SemanticVersion(other)); + return this.Version.IsNewerThan(other); } /// Get whether this version is between two specified versions (inclusively). @@ -112,7 +111,7 @@ namespace StardewModdingAPI /// The maximum version. public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { - return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + return this.Version.IsBetween(min, max); } /// Get whether this version is between two specified versions (inclusively). @@ -121,7 +120,7 @@ namespace StardewModdingAPI /// 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.Version.IsBetween(min, max); } /// Indicates whether the current object is equal to another object of the same type. @@ -144,7 +143,7 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. internal static bool TryParse(string version, out ISemanticVersion parsed) { - if (Toolkit.SemanticVersion.TryParse(version, out Toolkit.ISemanticVersion versionImpl)) + if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl)) { parsed = new SemanticVersion(versionImpl); return true; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a429d204..c872391c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -280,7 +280,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs new file mode 100644 index 00000000..961ef777 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -0,0 +1,62 @@ +using System; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + int PatchVersion { get; } + + /// An optional build tag. + string Build { get; } + + + /********* + ** Accessors + *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsOlderThan(string other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsNewerThan(string other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + bool IsBetween(string min, string max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs deleted file mode 100644 index ca62d393..00000000 --- a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - int Patch { get; } - - /// An optional prerelease tag. - string Tag { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index de480416..156d58ce 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -31,16 +31,16 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// The major version incremented for major API changes. - public int Major { get; } + public int MajorVersion { get; } /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } + public int MinorVersion { get; } /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } + public int PatchVersion { get; } /// An optional prerelease tag. - public string Tag { get; } + public string Build { get; } /********* @@ -53,10 +53,10 @@ namespace StardewModdingAPI.Toolkit /// An optional prerelease tag. public SemanticVersion(int major, int minor, int patch, string tag = null) { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.Build = this.GetNormalisedTag(tag); this.AssertValid(); } @@ -69,9 +69,9 @@ namespace StardewModdingAPI.Toolkit if (version == null) throw new ArgumentNullException(nameof(version), "The input version can't be null."); - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; + this.MajorVersion = version.Major; + this.MinorVersion = version.Minor; + this.PatchVersion = version.Build; this.AssertValid(); } @@ -90,10 +90,10 @@ namespace StardewModdingAPI.Toolkit throw new FormatException($"The input '{version}' isn't a valid semantic version."); // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + 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.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; this.AssertValid(); } @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Toolkit { if (other == null) throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); } /// Indicates whether the current object is equal to another object of the same type. @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Toolkit /// Whether this is a pre-release version. public bool IsPrerelease() { - return !string.IsNullOrWhiteSpace(this.Tag); + return !string.IsNullOrWhiteSpace(this.Build); } /// Get whether this version is older than the specified version. @@ -129,6 +129,14 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other) < 0; } + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsOlderThan(string other) + { + return this.IsOlderThan(new SemanticVersion(other)); + } + /// Get whether this version is newer than the specified version. /// The version to compare with this instance. public bool IsNewerThan(ISemanticVersion other) @@ -136,6 +144,14 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other) > 0; } + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsNewerThan(string other) + { + return this.IsNewerThan(new SemanticVersion(other)); + } + /// Get whether this version is between two specified versions (inclusively). /// The minimum version. /// The maximum version. @@ -144,16 +160,25 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; } + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// 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)); + } + /// Get a string representation of the version. public override string ToString() { // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; + string result = this.PatchVersion != 0 + ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" + : $"{this.MajorVersion}.{this.MinorVersion}"; // tag - string tag = this.Tag; + string tag = this.Build; if (tag != null) result += $"-{tag}"; return result; @@ -201,17 +226,17 @@ namespace StardewModdingAPI.Toolkit const int curOlder = -1; // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) + if (this.MajorVersion != otherMajor) + return this.MajorVersion.CompareTo(otherMajor); + if (this.MinorVersion != otherMinor) + return this.MinorVersion.CompareTo(otherMinor); + if (this.PatchVersion != otherPatch) + return this.PatchVersion.CompareTo(otherPatch); + if (this.Build == otherTag) return same; // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool curIsStable = string.IsNullOrWhiteSpace(this.Build); bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); if (curIsStable) return curNewer; @@ -219,7 +244,7 @@ namespace StardewModdingAPI.Toolkit return curOlder; // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); + string[] curParts = this.Build.Split('.', '-'); string[] otherParts = otherTag.Split('.', '-'); for (int i = 0; i < curParts.Length; i++) { @@ -248,15 +273,15 @@ namespace StardewModdingAPI.Toolkit /// Assert that the current version is valid. private void AssertValid() { - if (this.Major < 0 || this.Minor < 0 || this.Patch < 0) + if (this.MajorVersion < 0 || this.MinorVersion < 0 || this.PatchVersion < 0) throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); - if (this.Major == 0 && this.Minor == 0 && this.Patch == 0) + if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0) throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); - if (this.Tag != null) + if (this.Build != null) { - if (this.Tag.Trim() == "") + if (this.Build.Trim() == "") throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); - if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) + if (!Regex.IsMatch(this.Build, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); } } -- cgit From b08e27d13a1f0c82656df95212fc40588b3b5314 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:51:51 -0400 Subject: merge IManifest interfaces into new project (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 25 +++--- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 9 +- src/SMAPI/Framework/Models/Manifest.cs | 97 ---------------------- .../Framework/Models/ManifestContentPackFor.cs | 36 -------- src/SMAPI/Framework/Models/ManifestDependency.cs | 40 --------- .../Serialisation/SemanticVersionConverter.cs | 40 --------- src/SMAPI/IManifest.cs | 44 ---------- src/SMAPI/IManifestContentPackFor.cs | 12 --- src/SMAPI/IManifestDependency.cs | 18 ---- src/SMAPI/Metadata/InstructionMetadata.cs | 7 +- src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 7 -- .../IManifest.cs | 44 ++++++++++ .../IManifestContentPackFor.cs | 12 +++ .../IManifestDependency.cs | 18 ++++ .../Converters/SemanticVersionConverter.cs | 1 - .../Serialisation/Models/Manifest.cs | 37 +++++++-- .../Serialisation/Models/ManifestContentPackFor.cs | 8 +- .../Serialisation/Models/ManifestDependency.cs | 8 +- 20 files changed, 142 insertions(+), 328 deletions(-) delete mode 100644 src/SMAPI/Framework/Models/Manifest.cs delete mode 100644 src/SMAPI/Framework/Models/ManifestContentPackFor.cs delete mode 100644 src/SMAPI/Framework/Models/ManifestDependency.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs delete mode 100644 src/SMAPI/IManifest.cs delete mode 100644 src/SMAPI/IManifestContentPackFor.cs delete mode 100644 src/SMAPI/IManifestDependency.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs (limited to 'src') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 2fbeb9b6..a0fe2023 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -7,9 +7,9 @@ using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Core { @@ -472,17 +472,18 @@ namespace StardewModdingAPI.Tests.Core /// The value. private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) { - return new Manifest( - uniqueID: id ?? $"{Sample.String()}.{Sample.String()}", - name: name ?? id ?? Sample.String(), - author: Sample.String(), - description: Sample.String(), - version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - entryDll: entryDll ?? $"{Sample.String()}.dll", - contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID) : null, - minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - dependencies: dependencies - ); + return new Manifest + { + UniqueID = id ?? $"{Sample.String()}.{Sample.String()}", + Name = name ?? id ?? Sample.String(), + Author = Sample.String(), + Description = Sample.String(), + Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + EntryDll = entryDll ?? $"{Sample.String()}.dll", + ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, + MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, + Dependencies = dependencies + }; } /// Get a randomised basic manifest. diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 18904857..d9498e83 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers @@ -185,7 +185,7 @@ namespace StardewModdingAPI.Framework.ModHelpers author: author, description: description, version: version, - contentPackFor: new ManifestContentPackFor(this.ModID) + contentPackFor: this.ModID ); // create content pack diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 3366e8c1..fde921e6 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -4,10 +4,9 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; -using ToolkitManifest = StardewModdingAPI.Toolkit.Serialisation.Models.Manifest; namespace StardewModdingAPI.Framework.ModLoading { @@ -33,15 +32,13 @@ namespace StardewModdingAPI.Framework.ModLoading string path = Path.Combine(modDir.FullName, "manifest.json"); try { - ToolkitManifest rawManifest = jsonHelper.ReadJsonFile(path); - if (rawManifest == null) + manifest = jsonHelper.ReadJsonFile(path); + if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else - manifest = new Manifest(rawManifest); } catch (SParseException ex) { diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs deleted file mode 100644 index 92ffe0dc..00000000 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// A manifest which describes a mod for SMAPI. - internal class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; } - - /// A brief description of the mod. - public string Description { get; } - - /// The mod author's name. - public string Author { get; } - - /// The mod version. - public ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; } - - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - public string EntryDll { get; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - public IManifestContentPackFor ContentPackFor { get; } - - /// The other mods that must be loaded before this mod. - public IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - public string[] UpdateKeys { get; set; } - - /// The unique mod ID. - public string UniqueID { get; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit manifest. - public Manifest(Toolkit.Serialisation.Models.Manifest manifest) - : this( - uniqueID: manifest.UniqueID, - name: manifest.Name, - author: manifest.Author, - description: manifest.Description, - version: manifest.Version != null ? new SemanticVersion(manifest.Version) : null, - entryDll: manifest.EntryDll, - minimumApiVersion: manifest.MinimumApiVersion != null ? new SemanticVersion(manifest.MinimumApiVersion) : null, - contentPackFor: manifest.ContentPackFor != null ? new ManifestContentPackFor(manifest.ContentPackFor) : null, - dependencies: manifest.Dependencies?.Select(p => p != null ? (IManifestDependency)new ManifestDependency(p) : null).ToArray(), - updateKeys: manifest.UpdateKeys, - extraFields: manifest.ExtraFields - ) - { } - - /// Construct an instance for a transitional content pack. - /// The unique mod ID. - /// The mod name. - /// The mod author's name. - /// A brief description of the mod. - /// The mod version. - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - /// The minimum SMAPI version required by this mod, if any. - /// The modID which will read this as a content pack. Mutually exclusive with . - /// The other mods that must be loaded before this mod. - /// The namespaced mod IDs to query for updates (like Nexus:541). - /// Any manifest fields which didn't match a valid field. - public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string entryDll = null, ISemanticVersion minimumApiVersion = null, IManifestContentPackFor contentPackFor = null, IManifestDependency[] dependencies = null, string[] updateKeys = null, IDictionary extraFields = null) - { - this.Name = name; - this.Author = author; - this.Description = description; - this.Version = version; - this.UniqueID = uniqueID; - this.UpdateKeys = new string[0]; - this.EntryDll = entryDll; - this.ContentPackFor = contentPackFor; - this.MinimumApiVersion = minimumApiVersion; - this.Dependencies = dependencies ?? new IManifestDependency[0]; - this.UpdateKeys = updateKeys ?? new string[0]; - this.ExtraFields = extraFields; - } - } -} diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs deleted file mode 100644 index 90e20c6a..00000000 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - internal class ManifestContentPackFor : IManifestContentPackFor - { - /********* - ** Accessors - *********/ - /// The unique ID of the mod which can read this content pack. - public string UniqueID { get; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit instance. - public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) - { - this.UniqueID = contentPackFor.UniqueID; - this.MinimumVersion = contentPackFor.MinimumVersion != null ? new SemanticVersion(contentPackFor.MinimumVersion) : null; - } - - /// Construct an instance. - /// The unique ID of the mod which can read this content pack. - /// The minimum required version (if any). - public ManifestContentPackFor(string uniqueID, ISemanticVersion minimumVersion = null) - { - this.UniqueID = uniqueID; - this.MinimumVersion = minimumVersion; - } - } -} diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs deleted file mode 100644 index e92597f3..00000000 --- a/src/SMAPI/Framework/Models/ManifestDependency.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A mod dependency listed in a mod manifest. - internal class ManifestDependency : IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - public string UniqueID { get; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit instance. - public ManifestDependency(Toolkit.Serialisation.Models.ManifestDependency dependency) - : this(dependency.UniqueID, dependency.MinimumVersion?.ToString(), dependency.IsRequired) { } - - /// Construct an instance. - /// The unique mod ID to require. - /// The minimum required version (if any). - /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) - { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; - this.IsRequired = required; - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs deleted file mode 100644 index 3e05a440..00000000 --- a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) - { - int major = obj.ValueIgnoreCase("MajorVersion"); - int minor = obj.ValueIgnoreCase("MinorVersion"); - int patch = obj.ValueIgnoreCase("PatchVersion"); - string build = obj.ValueIgnoreCase("Build"); - if (build == "0") - build = null; // '0' from incorrect examples in old SMAPI documentation - - return new SemanticVersion(major, minor, patch, build); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) - throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); - return version; - } - } -} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs deleted file mode 100644 index 6c07d374..00000000 --- a/src/SMAPI/IManifest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// A manifest which describes a mod for SMAPI. - public interface IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - string Name { get; } - - /// A brief description of the mod. - string Description { get; } - - /// The mod author's name. - string Author { get; } - - /// The mod version. - ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - ISemanticVersion MinimumApiVersion { get; } - - /// The unique mod ID. - string UniqueID { get; } - - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - string EntryDll { get; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - IManifestContentPackFor ContentPackFor { get; } - - /// The other mods that must be loaded before this mod. - IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; } - - /// Any manifest fields which didn't match a valid field. - IDictionary ExtraFields { get; } - } -} diff --git a/src/SMAPI/IManifestContentPackFor.cs b/src/SMAPI/IManifestContentPackFor.cs deleted file mode 100644 index f05a3873..00000000 --- a/src/SMAPI/IManifestContentPackFor.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - public interface IManifestContentPackFor - { - /// The unique ID of the mod which can read this content pack. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - } -} diff --git a/src/SMAPI/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs deleted file mode 100644 index e86cd1f4..00000000 --- a/src/SMAPI/IManifestDependency.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI -{ - /// A mod dependency listed in a mod manifest. - public interface IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - bool IsRequired { get; } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 543d44a7..c5128eb1 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -37,8 +37,11 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), - // rewrite for SMAPI 2.6 - new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // moved to SMAPI.Toolkit.CoreInterfaces + // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces) + new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a51d6380..1b276988 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -201,8 +201,7 @@ namespace StardewModdingAPI new StringEnumConverter(), new ColorConverter(), new PointConverter(), - new RectangleConverter(), - new Framework.Serialisation.SemanticVersionConverter() + new RectangleConverter() }; foreach (JsonConverter converter in converters) this.JsonHelper.JsonSettings.Converters.Add(converter); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c872391c..4852f70c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -126,11 +126,7 @@ - - - - @@ -186,7 +182,6 @@ - @@ -258,7 +253,6 @@ - @@ -275,7 +269,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs new file mode 100644 index 00000000..6c07d374 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// A manifest which describes a mod for SMAPI. + public interface IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + string Name { get; } + + /// A brief description of the mod. + string Description { get; } + + /// The mod author's name. + string Author { get; } + + /// The mod version. + ISemanticVersion Version { get; } + + /// The minimum SMAPI version required by this mod, if any. + ISemanticVersion MinimumApiVersion { get; } + + /// The unique mod ID. + string UniqueID { get; } + + /// The name of the DLL in the directory that has the method. Mutually exclusive with . + string EntryDll { get; } + + /// The mod which will read this as a content pack. Mutually exclusive with . + IManifestContentPackFor ContentPackFor { get; } + + /// The other mods that must be loaded before this mod. + IManifestDependency[] Dependencies { get; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; } + + /// Any manifest fields which didn't match a valid field. + IDictionary ExtraFields { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public interface IManifestContentPackFor + { + /// The unique ID of the mod which can read this content pack. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs new file mode 100644 index 00000000..e86cd1f4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI +{ + /// A mod dependency listed in a mod manifest. + public interface IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 2ddaa1bf..eff95d1f 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs index 68987dd1..6ec57258 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Serialisation.Converters; namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A manifest which describes a mod for SMAPI. - public class Manifest + public class Manifest : IManifest { /********* ** Accessors @@ -20,21 +20,23 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string Author { get; set; } /// The mod version. - public SemanticVersion Version { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - public SemanticVersion MinimumApiVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . public string EntryDll { get; set; } /// The mod which will read this as a content pack. Mutually exclusive with . [JsonConverter(typeof(ManifestContentPackForConverter))] - public ManifestContentPackFor ContentPackFor { get; set; } + public IManifestContentPackFor ContentPackFor { get; set; } /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public ManifestDependency[] Dependencies { get; set; } + public IManifestDependency[] Dependencies { get; set; } /// The namespaced mod IDs to query for updates (like Nexus:541). public string[] UpdateKeys { get; set; } @@ -45,5 +47,30 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models /// Any manifest fields which didn't match a valid field. [JsonExtensionData] public IDictionary ExtraFields { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public Manifest() { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The modID which will read this as a content pack. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; + } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs index 00546533..64808dcf 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -1,7 +1,10 @@ +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// Indicates which mod can read the content pack represented by the containing manifest. - public class ManifestContentPackFor + public class ManifestContentPackFor : IManifestContentPackFor { /********* ** Accessors @@ -10,6 +13,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - public SemanticVersion MinimumVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumVersion { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs index d902f9ac..67e733dd 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -1,7 +1,10 @@ +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A mod dependency listed in a mod manifest. - public class ManifestDependency + public class ManifestDependency : IManifestDependency { /********* ** Accessors @@ -10,7 +13,8 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - public SemanticVersion MinimumVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumVersion { get; set; } /// Whether the dependency must be installed to use the mod. public bool IsRequired { get; set; } -- cgit From 85efb3112912a28dbdc82b18d0be8dd117f8c8ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 23:01:39 -0400 Subject: fix ISemanticVersion deserialisation errors (#532) --- .../Serialisation/Converters/SemanticVersionConverter.cs | 8 ++++---- src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs | 2 -- .../Serialisation/Models/ManifestContentPackFor.cs | 4 ---- .../Serialisation/Models/ManifestDependency.cs | 4 ---- 4 files changed, 4 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index eff95d1f..4f0949fa 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -2,8 +2,8 @@ using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter + /// Handles deserialisation of . + internal class SemanticVersionConverter : SimpleReadOnlyConverter { /********* ** Protected methods @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// Read a JSON object. /// The JSON object to read. /// The path to the current JSON node. - protected override SemanticVersion ReadObject(JObject obj, string path) + protected override ISemanticVersion ReadObject(JObject obj, string path) { int major = obj.ValueIgnoreCase("MajorVersion"); int minor = obj.ValueIgnoreCase("MinorVersion"); @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// Read a JSON string. /// The JSON string value. /// The path to the current JSON node. - protected override SemanticVersion ReadString(string str, string path) + protected override ISemanticVersion ReadString(string str, string path) { if (string.IsNullOrWhiteSpace(str)) return null; diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs index 6ec57258..6cb9496b 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -20,11 +20,9 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string Author { get; set; } /// The mod version. - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs index 64808dcf..d0e42216 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -1,6 +1,3 @@ -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// Indicates which mod can read the content pack represented by the containing manifest. @@ -13,7 +10,6 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumVersion { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs index 67e733dd..8db58d5d 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -1,6 +1,3 @@ -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A mod dependency listed in a mod manifest. @@ -13,7 +10,6 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumVersion { get; set; } /// Whether the dependency must be installed to use the mod. -- cgit From 08b37c70a35edd413e0da0c408e77d255200cf63 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 23:04:23 -0400 Subject: move type match lambda up into TypeFinder (#532) --- src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs | 10 ++++++++-- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 18 +++++++----------- src/SMAPI/Metadata/InstructionMetadata.cs | 8 ++++---- 3 files changed, 19 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index 45349def..79045241 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; @@ -16,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The result to return for matching instructions. private readonly InstructionHandleResult Result; + /// A lambda which overrides a matched type. + protected readonly Func ShouldIgnore; + /********* ** Accessors @@ -30,11 +34,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The full type name to match. /// The result to return for matching instructions. - public TypeFinder(string fullTypeName, InstructionHandleResult result) + /// A lambda which overrides a matched type. + public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) { this.FullTypeName = fullTypeName; this.Result = result; this.NounPhrase = $"{fullTypeName} type"; + this.ShouldIgnore = shouldIgnore ?? (p => false); } /// Perform the predefined logic for a method if applicable. @@ -113,7 +119,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders protected bool IsMatch(TypeReference type) { // root type - if (type.FullName == this.FullTypeName) + if (type.FullName == this.FullTypeName && !this.ShouldIgnore(type)) return true; // generic arguments diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index de9c439a..1bef4df4 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -17,9 +17,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The new type to reference. private readonly Type ToType; - /// A lambda which indicates whether a matching type reference should be rewritten. - private readonly Func ShouldRewrite; - /********* ** Public methods @@ -27,13 +24,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - /// A lambda which indicates whether a matching type reference should be rewritten. - public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldRewrite = null) - : base(fromTypeFullName, InstructionHandleResult.None) + /// A lambda which overrides a matched type. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) + : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore) { this.FromTypeName = fromTypeFullName; this.ToType = toType; - this.ShouldRewrite = shouldRewrite ?? (type => true); } /// Perform the predefined logic for a method if applicable. @@ -138,22 +134,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type to replace if it matches. private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type) { - // root type + // current type if (type.FullName == this.FromTypeName) { - if (!this.ShouldRewrite(type)) + if (this.ShouldIgnore(type)) return type; return module.ImportReference(this.ToType); } - // generic arguments + // recurse into generic arguments if (type is GenericInstanceType genericType) { for (int i = 0; i < genericType.GenericArguments.Count; i++) genericType.GenericArguments[i] = this.RewriteIfNeeded(module, genericType.GenericArguments[i]); } - // generic parameters (e.g. constraints) + // recurse into generic parameters (e.g. constraints) for (int i = 0; i < type.GenericParameters.Count; i++) type.GenericParameters[i] = new GenericParameter(this.RewriteIfNeeded(module, type.GenericParameters[i])); diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index c5128eb1..aa3e743c 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI.Metadata new VirtualEntryCallRemover(), // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces) - new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), type => type.Scope.Name == "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), type => type.Scope.Name == "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), type => type.Scope.Name == "StardewModdingAPI"), - new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), shouldIgnore: type => type.Scope.Name != "StardewModdingAPI"), // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), -- cgit From 06437b80aca329e749da2af79f4ed4d1a24db1b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 23:08:06 -0400 Subject: only rewrite type references if necessary (#532) --- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 43 +++++++++------------- 1 file changed, 18 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 1bef4df4..cf840dcc 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters // return type if (this.IsMatch(method.ReturnType)) { - method.ReturnType = this.RewriteIfNeeded(module, method.ReturnType); + this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); rewritten = true; } @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { if (this.IsMatch(parameter.ParameterType)) { - parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType); + this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); rewritten = true; } } @@ -64,9 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters var parameter = method.GenericParameters[i]; if (this.IsMatch(parameter)) { - TypeReference newType = this.RewriteIfNeeded(module, parameter); - if (newType != parameter) - method.GenericParameters[i] = new GenericParameter(parameter.Name, newType); + this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); rewritten = true; } } @@ -76,7 +74,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { if (this.IsMatch(variable.VariableType)) { - variable.VariableType = this.RewriteIfNeeded(module, variable.VariableType); + this.RewriteIfNeeded(module, variable.VariableType, newType => variable.VariableType = newType); rewritten = true; } } @@ -101,27 +99,23 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null) { - fieldRef.DeclaringType = this.RewriteIfNeeded(module, fieldRef.DeclaringType); - fieldRef.FieldType = this.RewriteIfNeeded(module, fieldRef.FieldType); + this.RewriteIfNeeded(module, fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType); + this.RewriteIfNeeded(module, fieldRef.FieldType, newType => fieldRef.FieldType = newType); } // method reference MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); if (methodRef != null) { - methodRef.DeclaringType = this.RewriteIfNeeded(module, methodRef.DeclaringType); - methodRef.ReturnType = this.RewriteIfNeeded(module, methodRef.ReturnType); + this.RewriteIfNeeded(module, methodRef.DeclaringType, newType => methodRef.DeclaringType = newType); + this.RewriteIfNeeded(module, methodRef.ReturnType, newType => methodRef.ReturnType = newType); foreach (var parameter in methodRef.Parameters) - parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType); + this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); } // type reference if (instruction.Operand is TypeReference typeRef) - { - TypeReference newRef = this.RewriteIfNeeded(module, typeRef); - if (typeRef != newRef) - cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); - } + this.RewriteIfNeeded(module, typeRef, newType => cil.Replace(instruction, cil.Create(instruction.OpCode, newType))); return InstructionHandleResult.Rewritten; } @@ -129,31 +123,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /********* ** Private methods *********/ - /// Get the adjusted type reference if it matches, else the same value. + /// Change a type reference if needed. /// The assembly module containing the instruction. /// The type to replace if it matches. - private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type) + /// Assign the new type reference. + private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set) { // current type if (type.FullName == this.FromTypeName) { - if (this.ShouldIgnore(type)) - return type; - return module.ImportReference(this.ToType); + if (!this.ShouldIgnore(type)) + set(module.ImportReference(this.ToType)); + return; } // recurse into generic arguments if (type is GenericInstanceType genericType) { for (int i = 0; i < genericType.GenericArguments.Count; i++) - genericType.GenericArguments[i] = this.RewriteIfNeeded(module, genericType.GenericArguments[i]); + this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); } // recurse into generic parameters (e.g. constraints) for (int i = 0; i < type.GenericParameters.Count; i++) - type.GenericParameters[i] = new GenericParameter(this.RewriteIfNeeded(module, type.GenericParameters[i])); - - return type; + this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); } } } -- cgit From c99237e7451808b41bb598aa5242fd9a49bfcc57 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 23:33:21 -0400 Subject: add mod build config option to ignore custom files by regex (#549) --- docs/mod-build-config.md | 10 +++++++ src/SMAPI.ModBuildConfig/DeployModTask.cs | 33 +++++++++++++++++++++- .../Framework/ModFileManager.cs | 19 +++++++++---- src/SMAPI.ModBuildConfig/build/smapi.targets | 1 + src/SMAPI.ModBuildConfig/package.nuspec | 2 ++ 5 files changed, 58 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 74ee34e4..32762580 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -121,6 +121,16 @@ or you have multiple installs, you can specify the path yourself. There's two wa The configuration will check your custom path first, then fall back to the default paths (so it'll still compile on a different computer). +### Ignore files +If you don't want to include a file in the mod folder or release zip: +* Make sure it's not copied to the build output. For a DLL, make sure the reference is [not marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). +* Or add this to your `.csproj` file under the `\.txt$, \.pdf$ + ``` + This is a comma-delimited list of regular expression patterns. If any pattern matches a file's + relative path in your mod folder, that file won't be included. + ### Unit test projects **(upcoming in 2.1)** diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index a5725a81..73971279 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using StardewModdingAPI.ModBuildConfig.Framework; @@ -42,6 +44,9 @@ namespace StardewModdingAPI.ModBuildConfig [Required] public bool EnableModZip { get; set; } + /// Custom comma-separated regex patterns matching files to ignore when deploying or zipping the mod. + public string IgnoreModFilePatterns { get; set; } + /********* ** Public methods @@ -55,8 +60,11 @@ namespace StardewModdingAPI.ModBuildConfig try { + // parse ignore patterns + Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray(); + // get mod info - ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir); + ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns); // deploy mod files if (this.EnableModDeploy) @@ -91,6 +99,29 @@ namespace StardewModdingAPI.ModBuildConfig /********* ** Private methods *********/ + /// Get the custom ignore patterns provided by the user. + private IEnumerable GetCustomIgnorePatterns() + { + if (string.IsNullOrWhiteSpace(this.IgnoreModFilePatterns)) + yield break; + + foreach (string raw in this.IgnoreModFilePatterns.Split(',')) + { + Regex regex; + try + { + regex = new Regex(raw.Trim(), RegexOptions.IgnoreCase); + } + catch (Exception ex) + { + this.Log.LogWarning($"Ignored invalid <{nameof(this.IgnoreModFilePatterns)}> pattern {raw}:\n{ex}"); + continue; + } + + yield return regex; + } + } + /// Copy the mod files into the game's mod folder. /// The files to include. /// The folder path to create with the mod files. diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 41e0201d..524aeaf7 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Web.Script.Serialization; using StardewModdingAPI.Toolkit; @@ -26,8 +27,9 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /// Construct an instance. /// The folder containing the project files. /// The folder containing the build output. + /// Custom regex patterns matching files to ignore when deploying or zipping the mod. /// The mod package isn't valid. - public ModFileManager(string projectDir, string targetDir) + public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns) { this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -72,8 +74,8 @@ namespace StardewModdingAPI.ModBuildConfig.Framework if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n")) continue; - // ignore release zips - if (this.ShouldIgnore(file)) + // handle ignored files + if (this.ShouldIgnore(file, relativePath, ignoreFilePatterns)) continue; // add file @@ -142,8 +144,10 @@ namespace StardewModdingAPI.ModBuildConfig.Framework ** Private methods *********/ /// Get whether a build output file should be ignored. - /// The file info. - private bool ShouldIgnore(FileInfo file) + /// The file to check. + /// The file's relative path in the package. + /// Custom regex patterns matching files to ignore when deploying or zipping the mod. + private bool ShouldIgnore(FileInfo file, string relativePath, Regex[] ignoreFilePatterns) { return // release zips @@ -159,7 +163,10 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // OS metadata files || this.EqualsInvariant(file.Name, ".DS_Store") - || this.EqualsInvariant(file.Name, "Thumbs.db"); + || this.EqualsInvariant(file.Name, "Thumbs.db") + + // custom ignore patterns + || ignoreFilePatterns.Any(p => p.IsMatch(relativePath)); } /// Get a case-insensitive dictionary matching the given JSON. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 0869be66..9946e1a6 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -166,6 +166,7 @@ ProjectDir="$(ProjectDir)" TargetDir="$(TargetDir)" GameDir="$(GamePath)" + IgnoreModFilePatterns="$(IgnoreModFilePatterns)" /> diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index fa26875b..1b1b25fd 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -16,6 +16,8 @@ - Added support for Stardew Valley 1.3. - Added support for unit test projects. - Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3. + - Added option to ignore files by regex pattern. + - Added reference to new SMAPI DLL. -- cgit From 59c9f1c7bfa915901d977f73bc3e1575ff806c6c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 23:51:29 -0400 Subject: update compatibility list --- src/SMAPI/StardewModdingAPI.metadata.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index 61a5befb..fc512b7b 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -259,7 +259,8 @@ "Categorize Chests": { "ID": "CategorizeChests", - "Default | UpdateKey": "Nexus:1300" + "Default | UpdateKey": "Nexus:1300", + "~1.4.3-unofficial.2.mizzion | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) }, "Chefs Closet": { @@ -376,7 +377,7 @@ "Content Patcher": { "ID": "Pathoschild.ContentPatcher", "Default | UpdateKey": "Nexus:1915", - "~1.4-beta.2 | Status": "AssumeBroken" // broke in SDV 1.3 (in-game errors) + "~1.4-beta.5 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) }, "Cooking Skill": { -- cgit From 4e02a01d6925fc4f02377bb0867a374a6e2b7fc4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Jun 2018 00:00:13 -0400 Subject: dispose assembly data once mods are loaded --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 2 +- src/SMAPI/Program.cs | 176 ++++++++++++----------- 2 files changed, 90 insertions(+), 88 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index d41774a9..ba4c3f5c 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -134,7 +134,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// assemblies (especially with Mono). Since this is meant to be called on , /// the implicit assumption is that loading the exact assembly failed. /// - public Assembly ResolveAssembly(string name) + public static Assembly ResolveAssembly(string name) { string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) return AppDomain.CurrentDomain diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 1b276988..14783513 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -213,6 +213,9 @@ namespace StardewModdingAPI #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + // add more leniant assembly resolvers + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); + // override game SGame.MonitorDuringInitialisation = this.Monitor; SGame.ReflectorDuringInitialisation = this.Reflection; @@ -794,104 +797,103 @@ namespace StardewModdingAPI StringComparer.InvariantCultureIgnoreCase ); - // get assembly loaders - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor); - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); - InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - - // load from metadata - foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) + // load mods from metadata + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) { - // get basic info - IManifest manifest = metadata.Manifest; - this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid - : $" {metadata.DisplayName}...", LogLevel.Trace); - - // show warnings - if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) + InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); + foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod - string assemblyPath = metadata.Manifest?.EntryDll != null - ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) - : null; - Assembly modAssembly; - try - { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); - } - catch (IncompatibleInstructionException) // details already in trace logs - { - string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); + // get basic info + IManifest manifest = metadata.Manifest; + this.Monitor.Log(metadata.Manifest?.EntryDll != null + ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid + : $" {metadata.DisplayName}...", LogLevel.Trace); + + // show warnings + if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) + metadata.SetWarning(ModWarning.NoUpdateKeys); + + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } - TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); - continue; - } - catch (SAssemblyLoadFailedException ex) - { - TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); - continue; - } - catch (Exception ex) - { - TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); - continue; - } + // load mod + string assemblyPath = metadata.Manifest?.EntryDll != null + ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) + : null; + Assembly modAssembly; + try + { + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); + } + catch (IncompatibleInstructionException) // details already in trace logs + { + string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - // initialise mod - try - { - // get mod instance - if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); continue; + } + catch (SAssemblyLoadFailedException ex) + { + TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); + continue; + } + catch (Exception ex) + { + TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); + continue; + } - // get content packs - if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) - contentPacks = new IContentPack[0]; - - // init mod helpers - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IModHelper modHelper; + // initialise mod + try { - IModEvents events = new ModEvents(metadata, this.EventManager); - ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); - IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); - - IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) + // get mod instance + if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + continue; + + // get content packs + if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) + contentPacks = new IContentPack[0]; + + // init mod helpers + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IModHelper modHelper; { - IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); + IModEvents events = new ModEvents(metadata, this.EventManager); + ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); + IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); + IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); + + IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); + } + + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); - } - - // init mod - mod.ModManifest = manifest; - mod.Helper = modHelper; - mod.Monitor = monitor; + // init mod + mod.ModManifest = manifest; + mod.Helper = modHelper; + mod.Monitor = monitor; - // track mod - metadata.SetMod(mod); - this.ModRegistry.Add(metadata); - } - catch (Exception ex) - { - TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); + // track mod + metadata.SetMod(mod); + this.ModRegistry.Add(metadata); + } + catch (Exception ex) + { + TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); + } } } } -- cgit From bb614435f00d8556a9a41318850e439244367168 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Jun 2018 00:15:06 -0400 Subject: bump versions for experimental release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index a08c3091..37fbd6ce 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.17", + "Version": "2.6.0-beta.17.1", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index fac55a0b..425bfee1 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.17", + "Version": "2.6.0-beta.17.1", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index df70d603..f358cd31 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.17"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.17.1"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); -- cgit From 68e33dafca9b2600612a6280b15931de1130d755 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Jun 2018 00:16:32 -0400 Subject: fix build issue --- src/SMAPI.sln | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 005537de..99e93d62 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -87,6 +87,7 @@ Global {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.Build.0 = Debug|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.Build.0 = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 -- cgit From f9f098fbf818d6d9f77f539633ac7d1ed21594fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Jun 2018 00:48:21 -0400 Subject: bump versions for beta release --- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 1b1b25fd..789868a0 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0-beta-20180428 + 2.1.0-beta-20180625 Build package for SMAPI mods Pathoschild Pathoschild diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 37fbd6ce..f240faab 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.17.1", + "Version": "2.6.0-beta.18", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 425bfee1..7314b62a 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.17.1", + "Version": "2.6.0-beta.18", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index f358cd31..01b99d62 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.17.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.18"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); -- cgit From 929e2642409ce50efd77f7b27edf30fd3ea769b1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Jun 2018 18:28:55 -0400 Subject: fix update checks failing when some mods don't have a mod ID --- src/SMAPI/Framework/IModMetadata.cs | 3 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 8 ++++++++ src/SMAPI/Program.cs | 10 +++++----- 3 files changed, 16 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index d3ec0035..6281c052 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -83,6 +83,9 @@ namespace StardewModdingAPI.Framework /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); + /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). + bool HasID(); + /// Whether the mod has at least one update key set. bool HasUpdateKeys(); } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 02a77778..3a412009 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -133,6 +133,14 @@ namespace StardewModdingAPI.Framework.ModLoading return this.Manifest != null; } + /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). + public bool HasID() + { + return + this.HasManifest() + && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); + } + /// Whether the mod has at least one update key set. public bool HasUpdateKeys() { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 14783513..09d9969c 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -640,7 +640,7 @@ namespace StardewModdingAPI List searchMods = new List(); foreach (IModMetadata mod in mods) { - if (!mod.HasManifest()) + if (!mod.HasID()) continue; string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; @@ -657,7 +657,7 @@ namespace StardewModdingAPI foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) { // link to update-check data - if (!mod.HasManifest() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) + if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) continue; mod.SetUpdateData(result); @@ -665,8 +665,8 @@ namespace StardewModdingAPI if (result.Errors != null && result.Errors.Any()) { errors.AppendLine(result.Errors.Length == 1 - ? $" {mod.DisplayName} update error: {result.Errors[0]}" - : $" {mod.DisplayName} update errors:\n - {string.Join("\n - ", result.Errors)}" + ? $" {mod.DisplayName}: {result.Errors[0]}" + : $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}" ); } @@ -688,7 +688,7 @@ namespace StardewModdingAPI // show update errors if (errors.Length != 0) - this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors, LogLevel.Trace); + this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace); // show update alerts if (updates.Any()) -- cgit From 9f0cfee55632663473ee06c0586035cc47abe397 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Jun 2018 19:46:03 -0400 Subject: update Game1.Draw override --- src/SMAPI/Framework/SGame.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index abe7bbc5..57afec06 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -850,6 +850,7 @@ namespace StardewModdingAPI.Framework } this.RaisePostRender(); Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); @@ -858,9 +859,8 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - this.drawOverlays(Game1.spriteBatch); } - else if ((int)Game1.gameMode == 11) + else if (Game1.gameMode == (byte)11) { Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); @@ -875,9 +875,10 @@ namespace StardewModdingAPI.Framework if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); Game1.spriteBatch.End(); } + this.drawOverlays(Game1.spriteBatch); this.RaisePostRender(needsNewBatch: true); if ((double)Game1.options.zoomLevel != 1.0) { @@ -887,7 +888,6 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - this.drawOverlays(Game1.spriteBatch); } else if (Game1.showingEndOfNightStuff) { @@ -908,6 +908,7 @@ namespace StardewModdingAPI.Framework } this.RaisePostRender(); Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); @@ -916,9 +917,8 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - this.drawOverlays(Game1.spriteBatch); } - else if ((int)Game1.gameMode == 6 || (int)Game1.gameMode == 3 && Game1.currentLocation == null) + else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); string str1 = ""; @@ -933,6 +933,7 @@ namespace StardewModdingAPI.Framework int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); @@ -941,13 +942,12 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - this.drawOverlays(Game1.spriteBatch); //base.Draw(gameTime); } else { Microsoft.Xna.Framework.Rectangle rectangle; - if ((int)Game1.gameMode == 0) + if (Game1.gameMode == (byte)0) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); } @@ -1114,14 +1114,14 @@ namespace StardewModdingAPI.Framework Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); Size size2 = Game1.viewport.Size; if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_140; + goto label_139; } else - goto label_140; + goto label_139; } Game1.drawPlayerHeldObject(Game1.player); } - label_140: + label_139: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1217,7 +1217,7 @@ namespace StardewModdingAPI.Framework } if (Game1.currentBillboard != 0) this.drawBillboard(); - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) + if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) { this.Events.Graphics_OnPreRenderHudEvent.Raise(); this.drawHUD(); @@ -1263,7 +1263,7 @@ namespace StardewModdingAPI.Framework if (Game1.isRaining && Game1.currentLocation != null && ((bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))) Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); else if ((double)Game1.flashAlpha > 0.0) { if (Game1.options.screenFlash) -- cgit From 5f19e4f2035c36f9c6c882da3767d6f29409db1c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:05:53 -0400 Subject: move mod DB parsing into toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- src/SMAPI/Constants.cs | 26 ---- src/SMAPI/Framework/IModMetadata.cs | 2 +- src/SMAPI/Framework/ModData/ModDataField.cs | 82 ------------ src/SMAPI/Framework/ModData/ModDataFieldKey.cs | 18 --- src/SMAPI/Framework/ModData/ModDataRecord.cs | 146 --------------------- src/SMAPI/Framework/ModData/ModDatabase.cs | 140 -------------------- src/SMAPI/Framework/ModData/ModStatus.cs | 18 --- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 51 ------- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/Models/SMetadata.cs | 15 --- src/SMAPI/Program.cs | 9 +- src/SMAPI/StardewModdingAPI.csproj | 7 - .../Framework/ModData/ModDataField.cs | 82 ++++++++++++ .../Framework/ModData/ModDataFieldKey.cs | 18 +++ .../Framework/ModData/ModDataRecord.cs | 146 +++++++++++++++++++++ .../Framework/ModData/ModDatabase.cs | 140 ++++++++++++++++++++ .../Framework/ModData/ModStatus.cs | 18 +++ .../Framework/ModData/ParsedModDataRecord.cs | 51 +++++++ .../Framework/ModData/SMetadata.cs | 14 ++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 39 ++++++ 22 files changed, 517 insertions(+), 511 deletions(-) delete mode 100644 src/SMAPI/Framework/ModData/ModDataField.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDataFieldKey.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDataRecord.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDatabase.cs delete mode 100644 src/SMAPI/Framework/ModData/ModStatus.cs delete mode 100644 src/SMAPI/Framework/ModData/ParsedModDataRecord.cs delete mode 100644 src/SMAPI/Framework/Models/SMetadata.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs (limited to 'src') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index a0fe2023..e63057b3 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -6,8 +6,8 @@ using Moq; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 01b99d62..c4a3813e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -22,14 +21,6 @@ namespace StardewModdingAPI /// Whether the directory containing the current save's data exists on disk. private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. - private static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["GitHub"] = "https://github.com/{0}/releases", - ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" - }; - /********* ** Accessors @@ -159,23 +150,6 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } - /// Get an update URL for an update key (if valid). - /// The update key. - internal static string GetUpdateUrl(string updateKey) - { - string[] parts = updateKey.Split(new[] { ':' }, 2); - if (parts.Length != 2) - return null; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (Constants.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - return string.Format(urlTemplate, modID); - - return null; - } - /********* ** Private methods diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 6281c052..5a8689de 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ModData/ModDataField.cs b/src/SMAPI/Framework/ModData/ModDataField.cs deleted file mode 100644 index df906103..00000000 --- a/src/SMAPI/Framework/ModData/ModDataField.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Framework.ModData -{ - /// A versioned mod metadata field. - internal class ModDataField - { - /********* - ** Accessors - *********/ - /// The field key. - public ModDataFieldKey Key { get; } - - /// The field value. - public string Value { get; } - - /// Whether this field should only be applied if it's not already set. - public bool IsDefault { get; } - - /// The lowest version in the range, or null for all past versions. - public ISemanticVersion LowerVersion { get; } - - /// The highest version in the range, or null for all future versions. - public ISemanticVersion UpperVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The field key. - /// The field value. - /// Whether this field should only be applied if it's not already set. - /// The lowest version in the range, or null for all past versions. - /// The highest version in the range, or null for all future versions. - public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) - { - this.Key = key; - this.Value = value; - this.IsDefault = isDefault; - this.LowerVersion = lowerVersion; - this.UpperVersion = upperVersion; - } - - /// Get whether this data field applies for the given manifest. - /// The mod manifest. - public bool IsMatch(IManifest manifest) - { - return - manifest?.Version != null // ignore invalid manifest - && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) - && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); - } - - - /********* - ** Private methods - *********/ - /// Get whether a manifest field has a meaningful value for the purposes of enforcing . - /// The mod manifest. - /// The field key matching . - private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) - { - switch (key) - { - // update key - case ModDataFieldKey.UpdateKey: - return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); - - // non-manifest fields - case ModDataFieldKey.AlternativeUrl: - case ModDataFieldKey.StatusReasonPhrase: - case ModDataFieldKey.Status: - return false; - - default: - return false; - } - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs b/src/SMAPI/Framework/ModData/ModDataFieldKey.cs deleted file mode 100644 index f68f575c..00000000 --- a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.ModData -{ - /// The valid field keys. - public enum ModDataFieldKey - { - /// A manifest update key. - UpdateKey, - - /// An alternative URL the player can check for an updated version. - AlternativeUrl, - - /// The mod's predefined compatibility status. - Status, - - /// A reason phrase for the , or null to use the default reason. - StatusReasonPhrase - } -} diff --git a/src/SMAPI/Framework/ModData/ModDataRecord.cs b/src/SMAPI/Framework/ModData/ModDataRecord.cs deleted file mode 100644 index 56275f53..00000000 --- a/src/SMAPI/Framework/ModData/ModDataRecord.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Framework.ModData -{ - /// Raw mod metadata from SMAPI's internal mod list. - internal class ModDataRecord - { - /********* - ** Properties - *********/ - /// This field stores properties that aren't mapped to another field before they're parsed into . - [JsonExtensionData] - private IDictionary ExtensionData; - - - /********* - ** Accessors - *********/ - /// The mod's current unique ID. - public string ID { get; set; } - - /// The former mod IDs (if any). - /// - /// This uses a custom format which uniquely identifies a mod across multiple versions and - /// supports matching other fields if no ID was specified. This doesn't include the latest - /// ID, if any. Format rules: - /// 1. If the mod's ID changed over time, multiple variants can be separated by the - /// | character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and - /// EntryDll) to match. - /// - public string FormerIDs { get; set; } - - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - - /// The versioned field data. - /// - /// This maps field names to values. This should be accessed via . - /// Format notes: - /// - Each key consists of a field name prefixed with any combination of version range - /// and Default, separated by pipes (whitespace trimmed). For example, Name - /// will always override the name, Default | Name will only override a blank - /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. - /// - The version format is min~max (where either side can be blank for unbounded), or - /// a single version number. - /// - The field name itself corresponds to a value. - /// - public IDictionary Fields { get; set; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Get a parsed representation of the . - public IEnumerable GetFields() - { - foreach (KeyValuePair pair in this.Fields) - { - // init fields - string packedKey = pair.Key; - string value = pair.Value; - bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; - - // parse - string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); - ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); - foreach (string part in parts.Take(parts.Length - 1)) - { - // 'default' - if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) - { - isDefault = true; - continue; - } - - // version range - if (part.Contains("~")) - { - string[] versionParts = part.Split(new[] { '~' }, 2); - lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; - upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; - continue; - } - - // single version - lowerVersion = new SemanticVersion(part); - upperVersion = new SemanticVersion(part); - } - - yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); - } - } - - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) - { - // normalise version if possible - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) - version = parsed.ToString(); - - // fetch remote version - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - - - /********* - ** Private methods - *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) - { - if (this.ExtensionData != null) - { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; - } - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs deleted file mode 100644 index 62f37d9b..00000000 --- a/src/SMAPI/Framework/ModData/ModDatabase.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.ModData -{ - /// Handles access to SMAPI's internal mod metadata list. - internal class ModDatabase - { - /********* - ** Properties - *********/ - /// The underlying mod data records indexed by default display name. - private readonly IDictionary Records; - - /// Get an update URL for an update key (if valid). - private readonly Func GetUpdateUrl; - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModDatabase() - : this(new Dictionary(), key => null) { } - - /// Construct an instance. - /// The underlying mod data records indexed by default display name. - /// Get an update URL for an update key (if valid). - public ModDatabase(IDictionary records, Func getUpdateUrl) - { - this.Records = records; - this.GetUpdateUrl = getUpdateUrl; - } - - /// Get a parsed representation of the which match a given manifest. - /// The manifest to match. - public ParsedModDataRecord GetParsed(IManifest manifest) - { - // get raw record - if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record)) - return null; - - // parse fields - ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; - foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) - { - switch (field.Key) - { - // update key - case ModDataFieldKey.UpdateKey: - parsed.UpdateKey = field.Value; - break; - - // alternative URL - case ModDataFieldKey.AlternativeUrl: - parsed.AlternativeUrl = field.Value; - break; - - // status - case ModDataFieldKey.Status: - parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); - parsed.StatusUpperVersion = field.UpperVersion; - break; - - // status reason phrase - case ModDataFieldKey.StatusReasonPhrase: - parsed.StatusReasonPhrase = field.Value; - break; - } - } - - return parsed; - } - - /// Get the display name for a given mod ID (if available). - /// The unique mod ID. - public string GetDisplayNameFor(string id) - { - return this.TryGetRaw(id, out string displayName, out ModDataRecord _) - ? displayName - : null; - } - - /// Get the mod page URL for a mod (if available). - /// The unique mod ID. - public string GetModPageUrlFor(string id) - { - // get raw record - if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) - return null; - - // get update key - ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); - if (updateKeyField == null) - return null; - - // get update URL - return this.GetUpdateUrl(updateKeyField.Value); - } - - - /********* - ** Private models - *********/ - /// Get a raw data record. - /// The mod ID to match. - /// The mod's default display name. - /// The raw mod record. - private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) - { - if (!string.IsNullOrWhiteSpace(id)) - { - foreach (var entry in this.Records) - { - displayName = entry.Key; - record = entry.Value; - - // try main ID - if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - if (record.FormerIDs != null) - { - foreach (string part in record.FormerIDs.Split('|')) - { - if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - } - } - } - - displayName = null; - record = null; - return false; - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModStatus.cs b/src/SMAPI/Framework/ModData/ModStatus.cs deleted file mode 100644 index 0e1d94d4..00000000 --- a/src/SMAPI/Framework/ModData/ModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.ModData -{ - /// Indicates how SMAPI should treat a mod. - internal enum ModStatus - { - /// Don't override the status. - None, - - /// The mod is obsolete and shouldn't be used, regardless of version. - Obsolete, - - /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken, - - /// Assume the mod is compatible, even if SMAPI detects incompatible code. - AssumeCompatible - } -} diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs deleted file mode 100644 index 3801fac3..00000000 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace StardewModdingAPI.Framework.ModData -{ - /// A parsed representation of the fields from a for a specific manifest. - internal class ParsedModDataRecord - { - /********* - ** Accessors - *********/ - /// The underlying data record. - public ModDataRecord DataRecord { get; set; } - - /// The default mod name to display when the name isn't available (e.g. during dependency checks). - public string DisplayName { get; set; } - - /// The update key to apply. - public string UpdateKey { get; set; } - - /// The alternative URL the player can check for an updated version. - public string AlternativeUrl { get; set; } - - /// The predefined compatibility status. - public ModStatus Status { get; set; } = ModStatus.None; - - /// A reason phrase for the , or null to use the default reason. - public string StatusReasonPhrase { get; set; } - - /// The upper version for which the applies (if any). - public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) - { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : null; - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 3a412009..4db25932 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fde921e6..c1bc51ec 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; diff --git a/src/SMAPI/Framework/Models/SMetadata.cs b/src/SMAPI/Framework/Models/SMetadata.cs deleted file mode 100644 index 9ff495e9..00000000 --- a/src/SMAPI/Framework/Models/SMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using StardewModdingAPI.Framework.ModData; - -namespace StardewModdingAPI.Framework.Models -{ - /// The SMAPI predefined metadata. - internal class SMetadata - { - /******** - ** Accessors - ********/ - /// Extra metadata about mods. - public IDictionary ModData { get; set; } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 09d9969c..6f1fe761 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -21,7 +21,6 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; @@ -29,7 +28,9 @@ using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Converters; using StardewModdingAPI.Toolkit.Utilities; @@ -413,8 +414,8 @@ namespace StardewModdingAPI this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); // load mod data - SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiMetadataPath)); - ModDatabase modDatabase = new ModDatabase(metadata.ModData, Constants.GetUpdateUrl); + ModToolkit toolkit = new ModToolkit(); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath, toolkit.GetUpdateUrl); // load mods { @@ -423,7 +424,7 @@ namespace StardewModdingAPI // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl); + resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4852f70c..f849ee53 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -132,11 +132,6 @@ - - - - - @@ -239,7 +234,6 @@ - @@ -263,7 +257,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs new file mode 100644 index 00000000..b3954693 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A versioned mod metadata field. + public class ModDataField + { + /********* + ** Accessors + *********/ + /// The field key. + public ModDataFieldKey Key { get; } + + /// The field value. + public string Value { get; } + + /// Whether this field should only be applied if it's not already set. + public bool IsDefault { get; } + + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field key. + /// The field value. + /// Whether this field should only be applied if it's not already set. + /// The lowest version in the range, or null for all past versions. + /// The highest version in the range, or null for all future versions. + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// Get whether this data field applies for the given manifest. + /// The mod manifest. + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// Get whether a manifest field has a meaningful value for the purposes of enforcing . + /// The mod manifest. + /// The field key matching . + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs new file mode 100644 index 00000000..09dd0cc5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The valid field keys. + public enum ModDataFieldKey + { + /// A manifest update key. + UpdateKey, + + /// An alternative URL the player can check for an updated version. + AlternativeUrl, + + /// The mod's predefined compatibility status. + Status, + + /// A reason phrase for the , or null to use the default reason. + StatusReasonPhrase + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs new file mode 100644 index 00000000..97ad0ca4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Raw mod metadata from SMAPI's internal mod list. + public class ModDataRecord + { + /********* + ** Properties + *********/ + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + private IDictionary ExtensionData; + + + /********* + ** Accessors + *********/ + /// The mod's current unique ID. + public string ID { get; set; } + + /// The former mod IDs (if any). + /// + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// | character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// + public string FormerIDs { get; set; } + + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + /// The versioned field data. + /// + /// This maps field names to values. This should be accessed via . + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and Default, separated by pipes (whitespace trimmed). For example, Name + /// will always override the name, Default | Name will only override a blank + /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. + /// - The version format is min~max (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a value. + /// + public IDictionary Fields { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get a parsed representation of the . + public IEnumerable GetFields() + { + foreach (KeyValuePair pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) + ? new SemanticVersion(newVersion) + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + // normalise version if possible + if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + version = parsed.ToString(); + + // fetch remote version + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + + /********* + ** Private methods + *********/ + /// The method invoked after JSON deserialisation. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs new file mode 100644 index 00000000..c60d2bcb --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Handles access to SMAPI's internal mod metadata list. + public class ModDatabase + { + /********* + ** Properties + *********/ + /// The underlying mod data records indexed by default display name. + private readonly IDictionary Records; + + /// Get an update URL for an update key (if valid). + private readonly Func GetUpdateUrl; + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModDatabase() + : this(new Dictionary(), key => null) { } + + /// Construct an instance. + /// The underlying mod data records indexed by default display name. + /// Get an update URL for an update key (if valid). + public ModDatabase(IDictionary records, Func getUpdateUrl) + { + this.Records = records; + this.GetUpdateUrl = getUpdateUrl; + } + + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ParsedModDataRecord GetParsed(IManifest manifest) + { + // get raw record + if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record)) + return null; + + // parse fields + ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; + foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; + } + + /// Get the display name for a given mod ID (if available). + /// The unique mod ID. + public string GetDisplayNameFor(string id) + { + return this.TryGetRaw(id, out string displayName, out ModDataRecord _) + ? displayName + : null; + } + + /// Get the mod page URL for a mod (if available). + /// The unique mod ID. + public string GetModPageUrlFor(string id) + { + // get raw record + if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) + return null; + + // get update key + ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + if (updateKeyField == null) + return null; + + // get update URL + return this.GetUpdateUrl(updateKeyField.Value); + } + + + /********* + ** Private models + *********/ + /// Get a raw data record. + /// The mod ID to match. + /// The mod's default display name. + /// The raw mod record. + private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) + { + if (!string.IsNullOrWhiteSpace(id)) + { + foreach (var entry in this.Records) + { + displayName = entry.Key; + record = entry.Value; + + // try main ID + if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (record.FormerIDs != null) + { + foreach (string part in record.FormerIDs.Split('|')) + { + if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + } + } + + displayName = null; + record = null; + return false; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs new file mode 100644 index 00000000..09da74bf --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Indicates how SMAPI should treat a mod. + public enum ModStatus + { + /// Don't override the status. + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, + + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs new file mode 100644 index 00000000..74f11ea5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A parsed representation of the fields from a for a specific manifest. + public class ParsedModDataRecord + { + /********* + ** Accessors + *********/ + /// The underlying data record. + public ModDataRecord DataRecord { get; set; } + + /// The default mod name to display when the name isn't available (e.g. during dependency checks). + public string DisplayName { get; set; } + + /// The update key to apply. + public string UpdateKey { get; set; } + + /// The alternative URL the player can check for an updated version. + public string AlternativeUrl { get; set; } + + /// The predefined compatibility status. + public ModStatus Status { get; set; } = ModStatus.None; + + /// A reason phrase for the , or null to use the default reason. + public string StatusReasonPhrase { get; set; } + + /// The upper version for which the applies (if any). + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + { + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs new file mode 100644 index 00000000..9553cca9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The SMAPI predefined metadata. + internal class SMetadata + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 6136186e..1723991e 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -1,5 +1,10 @@ +using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; +using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Toolkit { @@ -12,6 +17,14 @@ namespace StardewModdingAPI.Toolkit /// The default HTTP user agent for the toolkit. private readonly string UserAgent; + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. + private readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["GitHub"] = "https://github.com/{0}/releases", + ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" + }; + /********* ** Public methods @@ -29,5 +42,31 @@ namespace StardewModdingAPI.Toolkit var client = new WikiCompatibilityClient(this.UserAgent); return await client.FetchAsync(); } + + /// Get SMAPI's internal mod database. + /// The file path for the SMAPI metadata file. + /// Get an update URL for an update key (if valid). + public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) + { + SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); + return new ModDatabase(metadata.ModData, getUpdateUrl); + } + + /// Get an update URL for an update key (if valid). + /// The update key. + internal string GetUpdateUrl(string updateKey) + { + string[] parts = updateKey.Split(new[] { ':' }, 2); + if (parts.Length != 2) + return null; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (this.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + return string.Format(urlTemplate, modID); + + return null; + } } } -- cgit From 82306a2c50f4d3df33d8ce62ca49628baf0cc3b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:40:31 -0400 Subject: encapsulate mod DB a bit better for use outside SMAPI (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 4 +- src/SMAPI/Framework/IModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +- .../Framework/ModData/MetadataModel.cs | 14 ++ .../Framework/ModData/ModDataModel.cs | 129 +++++++++++++++++ .../Framework/ModData/ModDataRecord.cs | 152 +++++++++------------ .../ModData/ModDataRecordVersionedFields.cs | 51 +++++++ .../Framework/ModData/ModDatabase.cs | 103 ++------------ .../Framework/ModData/ParsedModDataRecord.cs | 51 ------- .../Framework/ModData/SMetadata.cs | 14 -- src/StardewModdingAPI.Toolkit/ModToolkit.cs | 6 +- 12 files changed, 281 insertions(+), 253 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs (limited to 'src') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index e63057b3..9e91b993 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -142,7 +142,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ParsedModDataRecord + this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields { Status = ModStatus.AssumeBroken, AlternativeUrl = "http://example.org" @@ -526,7 +526,7 @@ namespace StardewModdingAPI.Tests.Core /// Set up a mock mod metadata for . /// The mock mod metadata. /// The extra metadata about the mod from SMAPI's internal data (if any). - private void SetupMetadataForValidation(Mock mod, ParsedModDataRecord modRecord = null) + private void SetupMetadataForValidation(Mock mod, ModDataRecordVersionedFields modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DataRecord).Returns(() => null); diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 5a8689de..2145105b 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework IManifest Manifest { get; } /// Metadata about the mod from SMAPI's internal data (if any). - ParsedModDataRecord DataRecord { get; } + ModDataRecordVersionedFields DataRecord { get; } /// The metadata resolution status. ModMetadataStatus Status { get; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 4db25932..585debb4 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// Metadata about the mod from SMAPI's internal data (if any). - public ParsedModDataRecord DataRecord { get; } + public ModDataRecordVersionedFields DataRecord { get; } /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's full directory path. /// The mod manifest. /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index c1bc51ec..174820a1 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -51,7 +51,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // parse internal data record (if any) - ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); // get display name string displayName = manifest?.Name; @@ -301,7 +301,7 @@ namespace StardewModdingAPI.Framework.ModLoading string[] failedModNames = ( from entry in dependencies where entry.IsRequired && entry.Mod == null - let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + let displayName = modDatabase.Get(entry.ID)?.DisplayName ?? entry.ID let modUrl = modDatabase.GetModPageUrlFor(entry.ID) orderby displayName select modUrl != null diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs new file mode 100644 index 00000000..ef6d4dd9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The SMAPI predefined metadata. + internal class MetadataModel + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs new file mode 100644 index 00000000..e2b3ec1d --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The raw mod metadata from SMAPI's internal mod list. + internal class ModDataModel + { + /********* + ** Accessors + *********/ + /// The mod's current unique ID. + public string ID { get; set; } + + /// The former mod IDs (if any). + /// + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// | character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// + public string FormerIDs { get; set; } + + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + public IDictionary ExtensionData { get; set; } + + /// The versioned field data. + /// + /// This maps field names to values. This should be accessed via . + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and Default, separated by pipes (whitespace trimmed). For example, Name + /// will always override the name, Default | Name will only override a blank + /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. + /// - The version format is min~max (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a value. + /// + public IDictionary Fields { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get a parsed representation of the . + public IEnumerable GetFields() + { + foreach (KeyValuePair pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// Get the former mod IDs. + public IEnumerable GetFormerIDs() + { + if (this.FormerIDs != null) + { + foreach (string id in this.FormerIDs.Split('|')) + yield return id.Trim(); + } + } + + + /********* + ** Private methods + *********/ + /// The method invoked after JSON deserialisation. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 97ad0ca4..21c9cfca 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -1,107 +1,66 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Toolkit.Framework.ModData { - /// Raw mod metadata from SMAPI's internal mod list. + /// The parsed mod metadata from SMAPI's internal mod list. public class ModDataRecord { - /********* - ** Properties - *********/ - /// This field stores properties that aren't mapped to another field before they're parsed into . - [JsonExtensionData] - private IDictionary ExtensionData; - - /********* ** Accessors *********/ + /// The mod's default display name. + public string DisplayName { get; } + /// The mod's current unique ID. - public string ID { get; set; } + public string ID { get; } /// The former mod IDs (if any). - /// - /// This uses a custom format which uniquely identifies a mod across multiple versions and - /// supports matching other fields if no ID was specified. This doesn't include the latest - /// ID, if any. Format rules: - /// 1. If the mod's ID changed over time, multiple variants can be separated by the - /// | character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and - /// EntryDll) to match. - /// - public string FormerIDs { get; set; } + public string[] FormerIDs { get; } /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + public IDictionary MapLocalVersions { get; } /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + public IDictionary MapRemoteVersions { get; } /// The versioned field data. - /// - /// This maps field names to values. This should be accessed via . - /// Format notes: - /// - Each key consists of a field name prefixed with any combination of version range - /// and Default, separated by pipes (whitespace trimmed). For example, Name - /// will always override the name, Default | Name will only override a blank - /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. - /// - The version format is min~max (where either side can be blank for unbounded), or - /// a single version number. - /// - The field name itself corresponds to a value. - /// - public IDictionary Fields { get; set; } = new Dictionary(); + public ModDataField[] Fields { get; } /********* ** Public methods *********/ - /// Get a parsed representation of the . - public IEnumerable GetFields() + /// Construct an instance. + /// The mod's default display name. + /// The raw data model. + internal ModDataRecord(string displayName, ModDataModel model) { - foreach (KeyValuePair pair in this.Fields) - { - // init fields - string packedKey = pair.Key; - string value = pair.Value; - bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; - - // parse - string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); - ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); - foreach (string part in parts.Take(parts.Length - 1)) - { - // 'default' - if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) - { - isDefault = true; - continue; - } - - // version range - if (part.Contains("~")) - { - string[] versionParts = part.Split(new[] { '~' }, 2); - lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; - upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; - continue; - } - - // single version - lowerVersion = new SemanticVersion(part); - upperVersion = new SemanticVersion(part); - } + this.DisplayName = displayName; + this.ID = model.ID; + this.FormerIDs = model.GetFormerIDs().ToArray(); + this.MapLocalVersions = new Dictionary(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); + this.MapRemoteVersions = new Dictionary(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); + this.Fields = model.GetFields().ToArray(); + } - yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + /// Get whether the mod has (or previously had) the given ID. + /// The mod ID. + public bool HasID(string id) + { + // try main ID + if (this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + foreach (string formerID in this.FormerIDs) + { + if (formerID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; } + + return false; } /// Get a semantic local version for update checks. @@ -127,20 +86,39 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData : version; } - - /********* - ** Private methods - *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) { - if (this.ExtensionData != null) + ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; + foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } } + + return parsed; } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs new file mode 100644 index 00000000..3601fc53 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The versioned fields from a for a specific manifest. + public class ModDataRecordVersionedFields + { + /********* + ** Accessors + *********/ + /// The underlying data record. + public ModDataRecord DataRecord { get; set; } + + /// The default mod name to display when the name isn't available (e.g. during dependency checks). + public string DisplayName { get; set; } + + /// The update key to apply. + public string UpdateKey { get; set; } + + /// The alternative URL the player can check for an updated version. + public string AlternativeUrl { get; set; } + + /// The predefined compatibility status. + public ModStatus Status { get; set; } = ModStatus.None; + + /// A reason phrase for the , or null to use the default reason. + public string StatusReasonPhrase { get; set; } + + /// The upper version for which the applies (if any). + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + { + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs index c60d2bcb..a12e3c67 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Properties *********/ /// The underlying mod data records indexed by default display name. - private readonly IDictionary Records; + private readonly ModDataRecord[] Records; /// Get an update URL for an update key (if valid). private readonly Func GetUpdateUrl; @@ -22,63 +22,23 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData *********/ /// Construct an empty instance. public ModDatabase() - : this(new Dictionary(), key => null) { } + : this(new ModDataRecord[0], key => null) { } /// Construct an instance. /// The underlying mod data records indexed by default display name. /// Get an update URL for an update key (if valid). - public ModDatabase(IDictionary records, Func getUpdateUrl) + public ModDatabase(IEnumerable records, Func getUpdateUrl) { - this.Records = records; + this.Records = records.ToArray(); this.GetUpdateUrl = getUpdateUrl; } - /// Get a parsed representation of the which match a given manifest. - /// The manifest to match. - public ParsedModDataRecord GetParsed(IManifest manifest) + /// Get a mod data record. + /// The unique mod ID. + public ModDataRecord Get(string modID) { - // get raw record - if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record)) - return null; - - // parse fields - ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; - foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) - { - switch (field.Key) - { - // update key - case ModDataFieldKey.UpdateKey: - parsed.UpdateKey = field.Value; - break; - - // alternative URL - case ModDataFieldKey.AlternativeUrl: - parsed.AlternativeUrl = field.Value; - break; - - // status - case ModDataFieldKey.Status: - parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); - parsed.StatusUpperVersion = field.UpperVersion; - break; - - // status reason phrase - case ModDataFieldKey.StatusReasonPhrase: - parsed.StatusReasonPhrase = field.Value; - break; - } - } - - return parsed; - } - - /// Get the display name for a given mod ID (if available). - /// The unique mod ID. - public string GetDisplayNameFor(string id) - { - return this.TryGetRaw(id, out string displayName, out ModDataRecord _) - ? displayName + return !string.IsNullOrWhiteSpace(modID) + ? this.Records.FirstOrDefault(p => p.HasID(modID)) : null; } @@ -86,55 +46,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The unique mod ID. public string GetModPageUrlFor(string id) { - // get raw record - if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) - return null; - // get update key - ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + ModDataRecord record = this.Get(id); + ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); if (updateKeyField == null) return null; // get update URL return this.GetUpdateUrl(updateKeyField.Value); } - - - /********* - ** Private models - *********/ - /// Get a raw data record. - /// The mod ID to match. - /// The mod's default display name. - /// The raw mod record. - private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) - { - if (!string.IsNullOrWhiteSpace(id)) - { - foreach (var entry in this.Records) - { - displayName = entry.Key; - record = entry.Value; - - // try main ID - if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - if (record.FormerIDs != null) - { - foreach (string part in record.FormerIDs.Split('|')) - { - if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - } - } - } - - displayName = null; - record = null; - return false; - } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs deleted file mode 100644 index 74f11ea5..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// A parsed representation of the fields from a for a specific manifest. - public class ParsedModDataRecord - { - /********* - ** Accessors - *********/ - /// The underlying data record. - public ModDataRecord DataRecord { get; set; } - - /// The default mod name to display when the name isn't available (e.g. during dependency checks). - public string DisplayName { get; set; } - - /// The update key to apply. - public string UpdateKey { get; set; } - - /// The alternative URL the player can check for an updated version. - public string AlternativeUrl { get; set; } - - /// The predefined compatibility status. - public ModStatus Status { get; set; } = ModStatus.None; - - /// A reason phrase for the , or null to use the default reason. - public string StatusReasonPhrase { get; set; } - - /// The upper version for which the applies (if any). - public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) - { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : null; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs deleted file mode 100644 index 9553cca9..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The SMAPI predefined metadata. - internal class SMetadata - { - /******** - ** Accessors - ********/ - /// Extra metadata about mods. - public IDictionary ModData { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 1723991e..7b678f3d 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -48,8 +49,9 @@ namespace StardewModdingAPI.Toolkit /// Get an update URL for an update key (if valid). public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) { - SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); - return new ModDatabase(metadata.ModData, getUpdateUrl); + MetadataModel metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); + ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); + return new ModDatabase(records, getUpdateUrl); } /// Get an update URL for an update key (if valid). -- cgit From b86a63015de6186b5f97cb868558c1550facb750 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:41:28 -0400 Subject: fix mod DB entry with no ID (#532) --- src/SMAPI/StardewModdingAPI.metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json index fc512b7b..343257f1 100644 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ b/src/SMAPI/StardewModdingAPI.metadata.json @@ -1305,7 +1305,8 @@ }, "Siv's Marriage Mod": { - "FormerIDs": "6266959802 | Siv.MarriageMod | medoli900.Siv's Marriage Mod", // changed in 1.2.3-unofficial versions + "ID": "6266959802", // official version + "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions "MapLocalVersions": { "0.0": "1.4" }, "Default | UpdateKey": "Nexus:366" }, -- cgit From 9f7b4e029668ccb7b05fb8b5b02b7a3998b05a80 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:55:45 -0400 Subject: add method to get all data records (#532) --- src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs index a12e3c67..3b98bcf1 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -33,6 +33,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData this.GetUpdateUrl = getUpdateUrl; } + /// Get all mod data records. + public IEnumerable GetAll() + { + return this.Records; + } + /// Get a mod data record. /// The unique mod ID. public ModDataRecord Get(string modID) -- cgit From cf3728562771b20b66c3f9307a72ae3b19b10bdf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 09:37:20 -0400 Subject: tweak log parser logic to handle new levels automatically --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 4d95901e..8151c502 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -5,6 +5,11 @@ @{ ViewData["Title"] = "SMAPI log parser"; IDictionary contentPacks = Model.GetContentPacksByMod(); + IDictionary defaultFilters = Enum + .GetValues(typeof(LogLevel)) + .Cast() + .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); + JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None }; } @section Head { @@ -21,15 +26,8 @@ smapi.logParser({ logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)), showPopup: @Json.Serialize(Model.ParsedLog == null), - showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }), - showLevels: { - @LogLevel.Trace.ToString().ToLower(): false, - @LogLevel.Debug.ToString().ToLower(): true, - @LogLevel.Info.ToString().ToLower(): true, - @LogLevel.Alert.ToString().ToLower(): true, - @LogLevel.Warn.ToString().ToLower(): true, - @LogLevel.Error.ToString().ToLower(): true - } + showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting), + showLevels: @Json.Serialize(defaultFilters, noFormatting) }, '@Model.SectionUrl'); }); -- cgit From 3e5c109df1f90904c2dcb177e35b35f003e90fd9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 09:47:31 -0400 Subject: add log parser option to view raw log --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/LogParserController.cs | 11 ++-- src/SMAPI.Web/ViewModels/LogParserModel.cs | 19 +++++- src/SMAPI.Web/Views/LogParser/Index.cshtml | 79 ++++++++++++++---------- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 4 ++ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 14 ++++- 6 files changed, 88 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5a9e4e92..329ea3ad 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -65,6 +65,7 @@ * Redesigned UI to be more mobile-friendly. * Added option to download from Nexus. * Changed log parser filters to show `DEBUG` messages by default. + * Added log parser option to view raw log. * Fixed log parser issue when content packs have no description. * Fixed log parser mangling crossplatform paths in some cases. diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 2bff1392..354bdb06 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -52,21 +52,22 @@ namespace StardewModdingAPI.Web.Controllers ***/ /// Render the log parser UI. /// The paste ID. + /// Whether to display the raw unparsed log. [HttpGet] [Route("log")] [Route("log/{id}")] - public async Task Index(string id = null) + public async Task Index(string id = null, bool raw = false) { // fresh page if (string.IsNullOrWhiteSpace(id)) - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null)); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id)); // log page PasteInfo paste = await this.GetAsync(id); ParsedLog log = paste.Success ? new LogParser().Parse(paste.Content) : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error }; - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log)); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log, raw)); } /*** @@ -80,7 +81,7 @@ namespace StardewModdingAPI.Web.Controllers // get raw log text string input = this.Request.Form["input"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(input)) - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null, null) { UploadError = "The log file seems to be empty." }); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." }); // upload log input = this.CompressString(input); @@ -88,7 +89,7 @@ namespace StardewModdingAPI.Web.Controllers // handle errors if (!result.Success) - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID, null) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); // redirect to view UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl)); diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index 0fbd8ad5..df36ca73 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -27,6 +27,9 @@ namespace StardewModdingAPI.Web.ViewModels /// The parsed log info. public ParsedLog ParsedLog { get; set; } + /// Whether to show the raw unparsed log. + public bool ShowRaw { get; set; } + /// An error which occurred while uploading the log to Pastebin. public string UploadError { get; set; } @@ -43,12 +46,24 @@ namespace StardewModdingAPI.Web.ViewModels /// Construct an instance. /// The root URL for the log parser controller. /// The paste ID. - /// The parsed log info. - public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog) + public LogParserModel(string sectionUrl, string pasteID) { this.SectionUrl = sectionUrl; this.PasteID = pasteID; + this.ParsedLog = null; + this.ShowRaw = false; + } + + /// Construct an instance. + /// The root URL for the log parser controller. + /// The paste ID. + /// The parsed log info. + /// Whether to show the raw unparsed log. + public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog, bool showRaw) + : this(sectionUrl, pasteID) + { this.ParsedLog = parsedLog; + this.ShowRaw = showRaw; } /// Get all content packs in the log grouped by the mod they're for. diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 8151c502..f5501fed 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -17,17 +17,18 @@ { } - + - + @@ -138,12 +139,15 @@ else if (Model.ParsedLog?.IsValid == true)
      - +
      @foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null)) { @@ -177,36 +181,47 @@ else if (Model.ParsedLog?.IsValid == true) }
      Installed mods: - click any mod to filter - show all - hide all + @if (!Model.ShowRaw) + { + click any mod to filter + show all + hide all + }
      -
      - Filter messages: - TRACE | - DEBUG | - INFO | - ALERT | - WARN | - ERROR -
      - - @foreach (var message in Model.ParsedLog.Messages) - { - string levelStr = message.Level.ToString().ToLower(); + @if (!Model.ShowRaw) + { +
      + Filter messages: + TRACE | + DEBUG | + INFO | + ALERT | + WARN | + ERROR +
      - - - - - - - if (message.Repeated > 0) +
      @message.Time@message.Level.ToString().ToUpper()@message.Mod@message.Text
      + @foreach (var message in Model.ParsedLog.Messages) { - - - + string levelStr = message.Level.ToString().ToLower(); + + + + + + + if (message.Repeated > 0) + { + + + + + } } - } -
      repeats [@message.Repeated] times.
      @message.Time@message.Level.ToString().ToUpper()@message.Mod@message.Text
      repeats [@message.Repeated] times.
      + + + view raw log + } + else + { +
      @Model.ParsedLog.RawText
      + view parsed log + }
    } else if (Model.ParsedLog?.IsValid == false) diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 09bb97f5..1fcd1bff 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -79,6 +79,10 @@ table#metadata, table#mods { cursor: pointer; } +#mods.filters-disabled tr { + cursor: default; +} + #metadata tr, #mods tr { background: #eee diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 44c3ad5d..0c654205 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -39,11 +39,17 @@ smapi.logParser = function (data, sectionUrl) { } }, methods: { - toggleLevel: function(id) { + toggleLevel: function (id) { + if (!data.enableFilters) + return; + this.showLevels[id] = !this.showLevels[id]; }, toggleMod: function (id) { + if (!data.enableFilters) + return; + var curShown = this.showMods[id]; // first filter: only show this by default @@ -64,6 +70,9 @@ smapi.logParser = function (data, sectionUrl) { }, showAllMods: function () { + if (!data.enableFilters) + return; + for (var key in this.showMods) { if (this.showMods.hasOwnProperty(key)) { this.showMods[key] = true; @@ -73,6 +82,9 @@ smapi.logParser = function (data, sectionUrl) { }, hideAllMods: function () { + if (!data.enableFilters) + return; + for (var key in this.showMods) { if (this.showMods.hasOwnProperty(key)) { this.showMods[key] = false; -- cgit From db7247f36d11c870ec7b54bf6ea8b5b036065288 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 20:51:49 -0400 Subject: simplify mod DB method (#532) --- src/SMAPI/Program.cs | 2 +- src/StardewModdingAPI.Toolkit/ModToolkit.cs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6f1fe761..150ed34a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -415,7 +415,7 @@ namespace StardewModdingAPI // load mod data ModToolkit toolkit = new ModToolkit(); - ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath, toolkit.GetUpdateUrl); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); // load mods { diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 7b678f3d..18fe1ff3 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -46,17 +46,16 @@ namespace StardewModdingAPI.Toolkit /// Get SMAPI's internal mod database. /// The file path for the SMAPI metadata file. - /// Get an update URL for an update key (if valid). - public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) + public ModDatabase GetModDatabase(string metadataPath) { MetadataModel metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); - return new ModDatabase(records, getUpdateUrl); + return new ModDatabase(records, this.GetUpdateUrl); } /// Get an update URL for an update key (if valid). /// The update key. - internal string GetUpdateUrl(string updateKey) + public string GetUpdateUrl(string updateKey) { string[] parts = updateKey.Split(new[] { ':' }, 2); if (parts.Length != 2) -- cgit From 583cb91f4a3429549b8e56081737e6a410ebd1a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 20:59:39 -0400 Subject: use mod DB in web API to get default update keys for mod IDs (#532) --- docs/release-notes.md | 15 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 22 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 5 + .../wwwroot/StardewModdingAPI.metadata.json | 1668 ++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +- src/SMAPI/StardewModdingAPI.metadata.json | 1668 -------------------- 6 files changed, 1707 insertions(+), 1678 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json delete mode 100644 src/SMAPI/StardewModdingAPI.metadata.json (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 329ea3ad..062f902e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,13 +3,16 @@ * For players: * Updated for Stardew Valley 1.3 (no longer compatible with earlier versions). * Added a bundled Save Backup mod. - * Added beta update channel. * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. - * Added console warning for mods which don't have update checks configured. - * Added update checks for optional mod files on Nexus. * Added `player_add name` command, which lets you add items to your inventory by name instead of ID. * Improved how mod warnings are shown in the console. + * Improved update checks: + * added beta update channel; + * added support for optional files on Nexus; + * added console warning for mods which don't have update checks configured; + * fixed mod update checks failing if a mod only has prerelease versions on GitHub; + * fixed Nexus mod update alerts not showing HTTPS links. * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. * Fixed installer error on Linux/Mac in some cases. @@ -20,9 +23,7 @@ * Fixed `world_setseason` command not running season-change logic. * Fixed `world_setseason` command not normalising the season value. * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). - * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) - * Fixed Nexus mod update alerts not showing HTTPS links. * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * Updated compatibility list. @@ -79,6 +80,10 @@ * Added Harmony for SMAPI's internal use to patch game functions for events. * Added metadata dump option in `StardewModdingAPI.config.json` for troubleshooting some cases. * Rewrote input suppression using new SDV 1.3 APIs. + * Rewrote update checks: + * Moved most logic into the web API. + * Changed web API to require mod ID. + * Changed web API to also fetch update keys from SMAPI's internal mod DB. * Rewrote world/player state tracking: * much more efficient than previous method; * uses net field events where available; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c5a1705d..960602f4 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -39,18 +42,23 @@ namespace StardewModdingAPI.Web.Controllers /// A regex which matches SMAPI-style semantic version. private readonly string VersionRegex; + /// The internal mod metadata list. + private readonly ModDatabase ModDatabase; + /********* ** Public methods *********/ /// Construct an instance. + /// The web hosting environment. /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. /// The GitHub API client. /// The Nexus API client. - public ModsApiController(IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) { + this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; this.Cache = cache; @@ -79,10 +87,20 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; + // resolve update keys + var updateKeys = new HashSet(mod.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + ModDataRecord record = this.ModDatabase.Get(mod.ID); + if (record?.Fields != null) + { + string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) + updateKeys.Add(defaultUpdateKey); + } + // get latest versions ModEntryModel result = new ModEntryModel { ID = mod.ID }; IList errors = new List(); - foreach (string updateKey in mod.UpdateKeys ?? new string[0]) + foreach (string updateKey in updateKeys) { // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index a409e6eb..6761c7ad 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -26,5 +26,10 @@ + + + PreserveNewest + + diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json new file mode 100644 index 00000000..343257f1 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -0,0 +1,1668 @@ +{ + /** + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. + * + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). + * + * - ID: the mod's latest unique ID (if any). + * + * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching + * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple + * variants can be separated with '|'. + * + * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. + * + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility + * automatically, since that hides the details from trace logs. + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. + */ + "ModData": { + "AccessChestAnywhere": { + "ID": "AccessChestAnywhere", + "MapLocalVersions": { "1.1-1078": "1.1" }, + "Default | UpdateKey": "Nexus:257", + "~1.1 | Status": "AssumeBroken" + }, + + "Adjust Artisan Prices": { + "ID": "ThatNorthernMonkey.AdjustArtisanPrices", + "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update + "MapRemoteVersions": { "0.01": "0.0.1" }, + "Default | UpdateKey": "Chucklefish:3532" + }, + + "Adjust Monster": { + "ID": "mmanlapat.AdjustMonster", + "Default | UpdateKey": "Nexus:1161" + }, + + "Advanced Location Loader": { + "ID": "Entoarox.AdvancedLocationLoader", + "~1.3.7 | UpdateKey": "Chucklefish:3619" // only enable update checks up to 1.3.7 by request (has its own update-check feature) + }, + + "Adventure Shop Inventory": { + "ID": "HammurabiAdventureShopInventory", + "Default | UpdateKey": "Chucklefish:4608" + }, + + "AgingMod": { + "ID": "skn.AgingMod", + "Default | UpdateKey": "Nexus:1129", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "All Crops All Seasons": { + "ID": "cantorsdust.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 + "Default | UpdateKey": "Nexus:170" + }, + + "All Professions": { + "ID": "cantorsdust.AllProfessions", + "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:174" + }, + + "Almighty Farming Tool": { + "ID": "439", + "MapRemoteVersions": { "1.21": "1.2.1" }, + "Default | UpdateKey": "Nexus:439" + }, + + "Animal Husbandry": { + "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 + "Default | UpdateKey": "Nexus:1538" + }, + + "Animal Mood Fix": { + "ID": "GPeters-AnimalMoodFix", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + + "Animal Sitter": { + "ID": "jwdred.AnimalSitter", + "Default | UpdateKey": "Nexus:581", + "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Arcade Pong": { + "ID": "Platonymous.ArcadePong", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + + "A Tapper's Dream": { + "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", + "Default | UpdateKey": "Nexus:260" + }, + + "Auto Animal Doors": { + "ID": "AaronTaggart.AutoAnimalDoors", + "Default | UpdateKey": "Nexus:1019" + }, + + "Auto-Eat": { + "ID": "Permamiss.AutoEat", + "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 + "Default | UpdateKey": "Nexus:643" + }, + + "AutoFish": { + "ID": "WhiteMind.AF", + "Default | UpdateKey": "Nexus:1895" + }, + + "AutoGate": { + "ID": "AutoGate", + "Default | UpdateKey": "Nexus:820" + }, + + "Automate": { + "ID": "Pathoschild.Automate", + "Default | UpdateKey": "Nexus:1063", + "~1.10-beta.7 | Status": "AssumeBroken" // broke in SDV 1.3.20 + }, + + "Automated Doors": { + "ID": "azah.automated-doors", + "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 + "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 + }, + + "AutoSpeed": { + "ID": "Omegasis.AutoSpeed", + "Default | UpdateKey": "Nexus:443" // added in 1.4.1 + }, + + "Basic Sprinklers Improved": { + "ID": "lrsk_sdvm_bsi.0117171308", + "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:833" + }, + + "Better Hay": { + "ID": "cat.betterhay", + "Default | UpdateKey": "Nexus:1430" + }, + + "Better Quality More Seasons": { + "ID": "SB_BQMS", + "Default | UpdateKey": "Nexus:935" + }, + + "Better Quarry": { + "ID": "BetterQuarry", + "Default | UpdateKey": "Nexus:771" + }, + + "Better Ranching": { + "ID": "BetterRanching", + "Default | UpdateKey": "Nexus:859" + }, + + "Better Shipping Box": { + "ID": "Kithio:BetterShippingBox", + "MapLocalVersions": { "1.0.1": "1.0.2" }, + "Default | UpdateKey": "Chucklefish:4302" + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "FormerIDs": "SPDSprinklersMod", // changed in 2.3 + "Default | UpdateKey": "Nexus:41" + }, + + "Billboard Anywhere": { + "ID": "Omegasis.BillboardAnywhere", + "Default | UpdateKey": "Nexus:492" // added in 1.4.1 + }, + + "Birthday Mail": { + "ID": "KathrynHazuka.BirthdayMail", + "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update + "Default | UpdateKey": "Nexus:276", + "MapRemoteVersions": { "1.3.1": "1.3" } // manifest not updated + }, + + "Breed Like Rabbits": { + "ID": "dycedarger.breedlikerabbits", + "Default | UpdateKey": "Nexus:948" + }, + + "Build Endurance": { + "ID": "Omegasis.BuildEndurance", + "Default | UpdateKey": "Nexus:445" // added in 1.4.1 + }, + + "Build Health": { + "ID": "Omegasis.BuildHealth", + "Default | UpdateKey": "Nexus:446" // added in 1.4.1 + }, + + "Buy Cooking Recipes": { + "ID": "Denifia.BuyRecipes", + "Default | UpdateKey": "Nexus:1126" // added in 1.0.1 (2017-10-04) + }, + + "Buy Back Collectables": { + "ID": "Omegasis.BuyBackCollectables", + "FormerIDs": "BuyBackCollectables", // changed in 1.4 + "Default | UpdateKey": "Nexus:507" // added in 1.4.1 + }, + + "Carry Chest": { + "ID": "spacechase0.CarryChest", + "Default | UpdateKey": "Nexus:1333" + }, + + "Casks Anywhere": { + "ID": "CasksAnywhere", + "MapLocalVersions": { "1.1-alpha": "1.1" }, + "Default | UpdateKey": "Nexus:878" + }, + + "Categorize Chests": { + "ID": "CategorizeChests", + "Default | UpdateKey": "Nexus:1300", + "~1.4.3-unofficial.2.mizzion | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) + }, + + "Chefs Closet": { + "ID": "Duder.ChefsCloset", + "MapLocalVersions": { "1.3-1": "1.3" }, + "Default | UpdateKey": "Nexus:1030" + }, + + "Chest Label System": { + "ID": "Speeder.ChestLabel", + "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update + "Default | UpdateKey": "Nexus:242" + }, + + "Chest Pooling": { + "ID": "mralbobo.ChestPooling", + "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling" + }, + + "Chests Anywhere": { + "ID": "Pathoschild.ChestsAnywhere", + "FormerIDs": "ChestsAnywhere", // changed in 1.9 + "Default | UpdateKey": "Nexus:518", + "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "CJB Automation": { + "ID": "CJBAutomation", + "Default | UpdateKey": "Nexus:211", + "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" + }, + + "CJB Cheats Menu": { + "ID": "CJBok.CheatsMenu", + "FormerIDs": "CJBCheatsMenu", // changed in 1.14 + "Default | UpdateKey": "Nexus:4", + "~1.18-beta | Status": "AssumeBroken" // broke in SDV 1.3, first beta causes significant friendship bugs + }, + + "CJB Item Spawner": { + "ID": "CJBok.ItemSpawner", + "FormerIDs": "CJBItemSpawner", // changed in 1.7 + "Default | UpdateKey": "Nexus:93", + "~1.10 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "CJB Show Item Sell Price": { + "ID": "CJBok.ShowItemSellPrice", + "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 + "Default | UpdateKey": "Nexus:5", + "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Clean Farm": { + "ID": "tstaples.CleanFarm", + "Default | UpdateKey": "Nexus:794" + }, + + "Climates of Ferngill": { + "ID": "KoihimeNakamura.ClimatesOfFerngill", + "Default | UpdateKey": "Nexus:604" + }, + + "Coal Regen": { + "ID": "Blucifer.CoalRegen", + "Default | UpdateKey": "Nexus:1664" + }, + + "Cobalt": { + "ID": "spacechase0.Cobalt", + "MapRemoteVersions": { "1.1.3": "1.1.2" } // not updated in manifest + }, + + "Cold Weather Haley": { + "ID": "LordXamon.ColdWeatherHaleyPRO", + "Default | UpdateKey": "Nexus:1169", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Colored Chests": { + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + + "Combat with Farm Implements": { + "ID": "SPDFarmingImplementsInCombat", + "Default | UpdateKey": "Nexus:313" + }, + + "Community Bundle Item Tooltip": { + "ID": "musbah.bundleTooltip", + "Default | UpdateKey": "Nexus:1329" + }, + + "Concentration on Farming": { + "ID": "punyo.ConcentrationOnFarming", + "Default | UpdateKey": "Nexus:1445" + }, + + "Configurable Machines": { + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "MapLocalVersions": { "1.2-beta": "1.2" }, + "Default | UpdateKey": "Nexus:280" + }, + + "Configurable Shipping Dates": { + "ID": "ConfigurableShippingDates", + "Default | UpdateKey": "Nexus:675", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Content Patcher": { + "ID": "Pathoschild.ContentPatcher", + "Default | UpdateKey": "Nexus:1915", + "~1.4-beta.5 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) + }, + + "Cooking Skill": { + "ID": "spacechase0.CookingSkill", + "FormerIDs": "CookingSkill", // changed in 1.0.4–6 + "Default | UpdateKey": "Nexus:522" + }, + + "CrabNet": { + "ID": "jwdred.CrabNet", + "Default | UpdateKey": "Nexus:584" + }, + + "Crafting Counter": { + "ID": "lolpcgaming.CraftingCounter", + "Default | UpdateKey": "Nexus:1585", + "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest + }, + + "Current Location": { + "ID": "CurrentLocation102120161203", + "Default | UpdateKey": "Nexus:638" + }, + + "Custom Asset Modifier": { + "ID": "Omegasis.CustomAssetModifier", + "Default | UpdateKey": "1836" + }, + + "Custom Critters": { + "ID": "spacechase0.CustomCritters", + "Default | UpdateKey": "Nexus:1255" + }, + + "Custom Crops": { + "ID": "spacechase0.CustomCrops", + "Default | UpdateKey": "Nexus:1592" + }, + + "Custom Element Handler": { + "ID": "Platonymous.CustomElementHandler", + "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 + }, + + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991" // added in 0.6.1 + }, + + "Custom Farming Automate Bridge": { + "ID": "Platonymous.CFAutomate", + "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate + "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" + }, + + "Custom Farm Types": { + "ID": "spacechase0.CustomFarmTypes", + "Default | UpdateKey": "Nexus:1140" + }, + + "Custom Furniture": { + "ID": "Platonymous.CustomFurniture", + "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 + }, + + "Customize Exterior": { + "ID": "spacechase0.CustomizeExterior", + "FormerIDs": "CustomizeExterior", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:1099", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Customizable Cart Redux": { + "ID": "KoihimeNakamura.CCR", + "MapLocalVersions": { "1.1-20170917": "1.1" }, + "Default | UpdateKey": "Nexus:1402" + }, + + "Customizable Traveling Cart Days": { + "ID": "TravelingCartYyeahdude", + "Default | UpdateKey": "Nexus:567" + }, + + "Custom Linens": { + "ID": "Mevima.CustomLinens", + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1027" + }, + + "Custom NPC": { + "ID": "Platonymous.CustomNPC", + "Default | UpdateKey": "Nexus:1607" + }, + + "Custom Shops Redux": { + "ID": "Omegasis.CustomShopReduxGui", + "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 + }, + + "Custom TV": { + "ID": "Platonymous.CustomTV", + "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 + }, + + "Daily Luck Message": { + "ID": "Schematix.DailyLuckMessage", + "Default | UpdateKey": "Nexus:1327" + }, + + "Daily News": { + "ID": "bashNinja.DailyNews", + "Default | UpdateKey": "Nexus:1141", + "~1.2 | Status": "AssumeBroken" // broke in Stardew Valley 1.3 (or depends on CustomTV which broke) + }, + + "Daily Quest Anywhere": { + "ID": "Omegasis.DailyQuestAnywhere", + "FormerIDs": "DailyQuest", // changed in 1.4 + "Default | UpdateKey": "Nexus:513" // added in 1.4.1 + }, + + "Data Maps": { + "ID": "Pathoschild.DataMaps", + "Default | UpdateKey": "Nexus:1691", + "~1.3 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Debug Mode": { + "ID": "Pathoschild.DebugMode", + "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 + "Default | UpdateKey": "Nexus:679", + "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Did You Water Your Crops?": { + "ID": "Nishtra.DidYouWaterYourCrops", + "Default | UpdateKey": "Nexus:1583" + }, + + "Dynamic Checklist": { + "ID": "gunnargolf.DynamicChecklist", + "Default | UpdateKey": "Nexus:1145" // added in 1.0.1-pathoschild-update + }, + + "Dynamic Horses": { + "ID": "Bpendragon-DynamicHorses", + "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:874" + }, + + "Dynamic Machines": { + "ID": "DynamicMachines", + "MapLocalVersions": { "1.1": "1.1.1" }, + "Default | UpdateKey": "Nexus:374", + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Dynamic NPC Sprites": { + "ID": "BashNinja.DynamicNPCSprites", + "Default | UpdateKey": "Nexus:1183" + }, + + "Easier Farming": { + "ID": "cautiouswafffle.EasierFarming", + "Default | UpdateKey": "Nexus:1426" + }, + + "Empty Hands": { + "ID": "QuicksilverFox.EmptyHands", + "Default | UpdateKey": "Nexus:1176" // added in 1.0.1-pathoschild-update + }, + + "Enemy Health Bars": { + "ID": "Speeder.HealthBars", + "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update + "Default | UpdateKey": "Nexus:193" + }, + + "Entoarox Framework": { + "ID": "Entoarox.EntoaroxFramework", + "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? + "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) + "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) + }, + + "Expanded Fridge": { + "ID": "Uwazouri.ExpandedFridge", + "Default | UpdateKey": "Nexus:1191" + }, + + "Experience Bars": { + "ID": "spacechase0.ExperienceBars", + "FormerIDs": "ExperienceBars", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:509" + }, + + "Extended Bus System": { + "ID": "ExtendedBusSystem", + "Default | UpdateKey": "Chucklefish:4373" + }, + + "Extended Fridge": { + "ID": "Crystalmir.ExtendedFridge", + "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 + "Default | UpdateKey": "Nexus:485" + }, + + "Extended Greenhouse": { + "ID": "ExtendedGreenhouse", + "Default | UpdateKey": "Chucklefish:4303", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Extended Minecart": { + "ID": "Entoarox.ExtendedMinecart", + "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) + }, + + "Extended Reach": { + "ID": "spacechase0.ExtendedReach", + "Default | UpdateKey": "Nexus:1493" + }, + + "Fall 28 Snow Day": { + "ID": "Omegasis.Fall28SnowDay", + "Default | UpdateKey": "Nexus:486", // added in 1.4.1 + "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything + }, + + "Farm Automation Unofficial: Item Collector": { + "ID": "Maddy99.FarmAutomation.ItemCollector", + "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Expansion": { + "ID": "Advize.FarmExpansion", + "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 + "Default | UpdateKey": "Nexus:130" + }, + + "Fast Animations": { + "ID": "Pathoschild.FastAnimations", + "Default | UpdateKey": "Nexus:1089", + "~1.5 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Faster Grass": { + "ID": "IceGladiador.FasterGrass", + "Default | UpdateKey": "Nexus:1772" + }, + + "Faster Paths": { + "ID": "Entoarox.FasterPaths", + "FormerIDs": "615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.3 + "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) + }, + + "Fishing Adjust": { + "ID": "shuaiz.FishingAdjustMod", + "Default | UpdateKey": "Nexus:1350", + "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' + }, + + "Fishing Tuner Redux": { + "ID": "HammurabiFishingTunerRedux", + "Default | UpdateKey": "Chucklefish:4578" + }, + + "Fixed Secret Woods Debris": { + "ID": "f4iTh.WoodsDebrisFix", + "Default | UpdateKey": "Nexus:1941" + }, + + "Flower Color Picker": { + "ID": "spacechase0.FlowerColorPicker", + "Default | UpdateKey": "Nexus:1229" + }, + + "Forage at the Farm": { + "ID": "Nishtra.ForageAtTheFarm", + "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 + "Default | UpdateKey": "Nexus:673" + }, + + "Furniture Anywhere": { + "ID": "Entoarox.FurnitureAnywhere", + "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) + }, + + "Game Reminder": { + "ID": "mmanlapat.GameReminder", + "Default | UpdateKey": "Nexus:1153" + }, + + "Gate Opener": { + "ID": "mralbobo.GateOpener", + "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener" + }, + + "GenericShopExtender": { + "ID": "GenericShopExtender", + "Default | UpdateKey": "Nexus:814" // added in 0.1.3 + }, + + "Geode Info Menu": { + "ID": "cat.geodeinfomenu", + "Default | UpdateKey": "Nexus:1448" + }, + + "Get Dressed": { + "ID": "Advize.GetDressed", + "Default | UpdateKey": "Nexus:331" + }, + + "Giant Crop Ring": { + "ID": "cat.giantcropring", + "Default | UpdateKey": "Nexus:1182" + }, + + "Gift Taste Helper": { + "ID": "tstaples.GiftTasteHelper", + "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 + "Default | UpdateKey": "Nexus:229" + }, + + "Grandfather's Gift": { + "ID": "ShadowDragon.GrandfathersGift", + "Default | UpdateKey": "Nexus:985" + }, + + "Happy Animals": { + "ID": "HappyAnimals", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Omegasis)": { + "ID": "Omegasis.HappyBirthday", + "Default | UpdateKey": "Nexus:520" // added in 1.4.1 + }, + + "Hardcore Mines": { + "ID": "kibbe.hardcore_mines", + "Default | UpdateKey": "Nexus:1674" + }, + + "Harp of Yoba Redux": { + "ID": "Platonymous.HarpOfYobaRedux", + "Default | UpdateKey": "Nexus:914" // added in 2.0.3 + }, + + "Harvest Moon Witch Princess": { + "ID": "Sasara.WitchPrincess", + "Default | UpdateKey": "Nexus:1157" + }, + + "Harvest With Scythe": { + "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", + "Default | UpdateKey": "Nexus:236" + }, + + "Horse Whistle (icepuente)": { + "ID": "icepuente.HorseWhistle", + "Default | UpdateKey": "Nexus:1131", + "~1.1.2-unofficial.1-pathoschild | Status": "AssumeBroken" // causes significant lag, fixed in unofficial.2 + }, + + "Hunger (Yyeadude)": { + "ID": "HungerYyeadude", + "Default | UpdateKey": "Nexus:613" + }, + + "Hunger for Food (Tigerle)": { + "ID": "HungerForFoodByTigerle", + "Default | UpdateKey": "Nexus:810", + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Hunger Mod (skn)": { + "ID": "skn.HungerMod", + "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1127" + }, + + "Idle Pause": { + "ID": "Veleek.IdlePause", + "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1092" + }, + + "Improved Quality of Life": { + "ID": "Demiacle.ImprovedQualityOfLife", + "Default | UpdateKey": "Nexus:1025", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Instant Geode": { + "ID": "InstantGeode", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Instant Grow Trees": { + "ID": "cantorsdust.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:173" + }, + + "Interaction Helper": { + "ID": "HammurabiInteractionHelper", + "Default | UpdateKey": "Chucklefish:4640" // added in 1.0.4-pathoschild-update + }, + + "Item Auto Stacker": { + "ID": "cat.autostacker", + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1184" + }, + + "Json Assets": { + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720" + }, + + "Junimo Farm": { + "ID": "Platonymous.JunimoFarm", + "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:984" // added in 1.1.3 + }, + + "Less Strict Over-Exertion (AntiExhaustion)": { + "ID": "BALANCEMOD_AntiExhaustion", + "MapLocalVersions": { "0.0": "1.1" }, + "Default | UpdateKey": "Nexus:637", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Level Extender": { + "ID": "DevinLematty.LevelExtender", + "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3 + "Default | UpdateKey": "Nexus:1471" + }, + + "Level Up Notifications": { + "ID": "Level Up Notifications", + "MapRemoteVersions": { "0.0.1a": "0.0.1" }, + "Default | UpdateKey": "Nexus:855" + }, + + "Location and Music Logging": { + "ID": "Brandy Lover.LMlog", + "Default | UpdateKey": "Nexus:1366" + }, + + "Longevity": { + "ID": "RTGOAT.Longevity", + "MapRemoteVersions": { "1.6.8h": "1.6.8" }, + "Default | UpdateKey": "Nexus:649" + }, + + "Lookup Anything": { + "ID": "Pathoschild.LookupAnything", + "FormerIDs": "LookupAnything", // changed in 1.10.1 + "Default | UpdateKey": "Nexus:541", + "~1.18.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Love Bubbles": { + "ID": "LoveBubbles", + "Default | UpdateKey": "Nexus:1318" + }, + + "Loved Labels": { + "ID": "Advize.LovedLabels", + "Default | UpdateKey": "Nexus:279" + }, + + "Luck Skill": { + "ID": "spacechase0.LuckSkill", + "FormerIDs": "LuckSkill", // changed in 0.1.4 + "Default | UpdateKey": "Nexus:521" + }, + + "Magic": { + "ID": "spacechase0.Magic", + "MapRemoteVersions": { "0.1.2": "0.1.1" } // not updated in manifest + }, + + "Mail Framework": { + "ID": "DIGUS.MailFrameworkMod", + "Default | UpdateKey": "Nexus:1536" + }, + + "MailOrderPigs": { + "ID": "jwdred.MailOrderPigs", + "Default | UpdateKey": "Nexus:632" + }, + + "Makeshift Multiplayer": { + "ID": "spacechase0.StardewValleyMP", + "FormerIDs": "StardewValleyMP", // changed in 0.3 + "Default | UpdateKey": "Nexus:501" + }, + + "Map Image Exporter": { + "ID": "spacechase0.MapImageExporter", + "FormerIDs": "MapImageExporter", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:1073" + }, + + "Message Box [API]? (ChatMod)": { + "ID": "Kithio:ChatMod", + "Default | UpdateKey": "Chucklefish:4296", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mining at the Farm": { + "ID": "Nishtra.MiningAtTheFarm", + "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 + "Default | UpdateKey": "Nexus:674" + }, + + "Mining With Explosives": { + "ID": "Nishtra.MiningWithExplosives", + "FormerIDs": "MiningWithExplosives", // changed in 1.1 + "Default | UpdateKey": "Nexus:770" + }, + + "Modder Serialization Utility": { + "ID": "SerializerUtils-0-1", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Monster Level Tip": { + "ID": "WhiteMind.MonsterLT", + "Default | UpdateKey": "Nexus:1896" + }, + + "More Animals": { + "ID": "Entoarox.MoreAnimals", + "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 + "~2.0.2 | UpdateKey": "Chucklefish:4288" // only enable update checks up to 2.0.2 by request (has its own update-check feature) + }, + + "More Artifact Spots": { + "ID": "451", + "Default | UpdateKey": "Nexus:451" + }, + + "More Map Layers": { + "ID": "Platonymous.MoreMapLayers", + "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 + }, + + "More Rain": { + "ID": "Omegasis.MoreRain", + "Default | UpdateKey": "Nexus:441", // added in 1.5.1 + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Weapons": { + "ID": "Joco80.MoreWeapons", + "Default | UpdateKey": "Nexus:1168" + }, + + "Move Faster": { + "ID": "shuaiz.MoveFasterMod", + "Default | UpdateKey": "Nexus:1351", + "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) + }, + + "Multiple Sprites and Portraits On Rotation (File Loading)": { + "ID": "FileLoading", + "MapLocalVersions": { "1.1": "1.12" }, + "Default | UpdateKey": "Nexus:1094", + "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Museum Rearranger": { + "ID": "Omegasis.MuseumRearranger", + "Default | UpdateKey": "Nexus:428" // added in 1.4.1 + }, + + "Mushroom Level Tip": { + "ID": "WhiteMind.MLT", + "Default | UpdateKey": "Nexus:1894" + }, + + "New Machines": { + "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", + "Default | UpdateKey": "Chucklefish:3683", + "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Night Owl": { + "ID": "Omegasis.NightOwl", + "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest + "Default | UpdateKey": "Nexus:433" // added in 1.4.1 + }, + + "No Crows": { + "ID": "cat.nocrows", + "Default | UpdateKey": "Nexus:1682" + }, + + "No Kids Ever": { + "ID": "Hangy.NoKidsEver", + "Default | UpdateKey": "Nexus:1464" + }, + + "No Debug Mode": { + "ID": "NoDebugMode", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, + + "No Fence Decay": { + "ID": "cat.nofencedecay", + "Default | UpdateKey": "Nexus:1180" + }, + + "No More Pets": { + "ID": "Omegasis.NoMorePets", + "FormerIDs": "NoMorePets", // changed in 1.4 + "Default | UpdateKey": "Nexus:506" // added in 1.4.1 + }, + + "No Rumble Horse": { + "ID": "Xangria.NoRumbleHorse", + "Default | UpdateKey": "Nexus:1779" + }, + + "No Soil Decay": { + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "Default | UpdateKey": "Nexus:237", + "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location + }, + + "No Soil Decay Redux": { + "ID": "Platonymous.NoSoilDecayRedux", + "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 + }, + + "NPC Map Locations": { + "ID": "Bouhm.NPCMapLocations", + "FormerIDs": "NPCMapLocationsMod", // changed in 2.0 + "Default | UpdateKey": "Nexus:239" + }, + + "Object Time Left": { + "ID": "spacechase0.ObjectTimeLeft", + "Default | UpdateKey": "Nexus:1315" + }, + + "OmniFarm": { + "ID": "PhthaloBlue.OmniFarm", + "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm" + }, + + "One Click Shed": { + "ID": "BitwiseJonMods.OneClickShedReloader", + "Default | UpdateKey": "Nexus:2052" + }, + + "Out of Season Bonuses (Seasonal Items)": { + "ID": "midoriarmstrong.seasonalitems", + "Default | UpdateKey": "Nexus:1452" + }, + + "Part of the Community": { + "ID": "SB_PotC", + "Default | UpdateKey": "Nexus:923" + }, + + "PelicanFiber": { + "ID": "jwdred.PelicanFiber", + "Default | UpdateKey": "Nexus:631" + }, + + "PelicanTTS": { + "ID": "Platonymous.PelicanTTS", + "Default | UpdateKey": "Nexus:1079" // added in 1.6.1 + }, + + "Persia the Mermaid - Standalone Custom NPC": { + "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", + "Default | UpdateKey": "Nexus:1419" + }, + + "Persistent Game Options": { + "ID": "Xangria.PersistentGameOptions", + "Default | UpdateKey": "Nexus:1778" + }, + + "Plant on Grass": { + "ID": "Demiacle.PlantOnGrass", + "Default | UpdateKey": "Nexus:1026" + }, + + "PyTK - Platonymous Toolkit": { + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" + }, + + "Point-and-Plant": { + "ID": "jwdred.PointAndPlant", + "Default | UpdateKey": "Nexus:572", + "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated + }, + + "Pony Weight Loss Program": { + "ID": "BadNetCode.PonyWeightLossProgram", + "Default | UpdateKey": "Nexus:1232" + }, + + "Portraiture": { + "ID": "Platonymous.Portraiture", + "Default | UpdateKey": "Nexus:999" // added in 1.3.1 + }, + + "Prairie King Made Easy": { + "ID": "Mucchan.PrairieKingMadeEasy", + "Default | UpdateKey": "Chucklefish:3594", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Purchasable Recipes": { + "ID": "Paracosm.PurchasableRecipes", + "Default | UpdateKey": "Nexus:1722" + }, + + "Quest Delay": { + "ID": "BadNetCode.QuestDelay", + "Default | UpdateKey": "Nexus:1239" + }, + + "Recatch Legendary Fish": { + "ID": "cantorsdust.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 + "Default | UpdateKey": "Nexus:172" + }, + + "Regeneration": { + "ID": "HammurabiRegeneration", + "Default | UpdateKey": "Chucklefish:4584" + }, + + "Relationship Bar UI": { + "ID": "RelationshipBar", + "Default | UpdateKey": "Nexus:1009" + }, + + "RelationshipsEnhanced": { + "ID": "relationshipsenhanced", + "Default | UpdateKey": "Chucklefish:4435", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Relationship Status": { + "ID": "relationshipstatus", + "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest + "Default | UpdateKey": "Nexus:751", + "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Rented Tools": { + "ID": "JarvieK.RentedTools", + "Default | UpdateKey": "Nexus:1307" + }, + + "Replanter": { + "ID": "jwdred.Replanter", + "Default | UpdateKey": "Nexus:589" + }, + + "ReRegeneration": { + "ID": "lrsk_sdvm_rerg.0925160827", + "MapLocalVersions": { "1.1.2-release": "1.1.2" }, + "Default | UpdateKey": "Chucklefish:4465" + }, + + "Reseed": { + "ID": "Roc.Reseed", + "Default | UpdateKey": "Nexus:887" + }, + + "Reusable Wallpapers and Floors (Wallpaper Retain)": { + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", + "Default | UpdateKey": "Nexus:356", + "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Ring of Fire": { + "ID": "Platonymous.RingOfFire", + "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 + }, + + "Rope Bridge": { + "ID": "RopeBridge", + "Default | UpdateKey": "Nexus:824" + }, + + "Rotate Toolbar": { + "ID": "Pathoschild.RotateToolbar", + "Default | UpdateKey": "Nexus:1100", + "~1.2.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Rush Orders": { + "ID": "spacechase0.RushOrders", + "FormerIDs": "RushOrders", // changed in 1.1 + "Default | UpdateKey": "Nexus:605" + }, + + "Save Anywhere": { + "ID": "Omegasis.SaveAnywhere", + "Default | UpdateKey": "Nexus:444", // added in 2.6.1 + "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest + }, + + "Save Backup": { + "ID": "Omegasis.SaveBackup", + "Default | UpdateKey": "Nexus:435", // added in 1.3.1 + "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Scroll to Blank": { + "ID": "caraxian.scroll.to.blank", + "Default | UpdateKey": "Chucklefish:4405" + }, + + "Scythe Harvesting": { + "ID": "mmanlapat.ScytheHarvesting", + "FormerIDs": "ScytheHarvesting", // changed in 1.6 + "Default | UpdateKey": "Nexus:1106" + }, + + "SDV Twitch": { + "ID": "MTD.SDVTwitch", + "Default | UpdateKey": "Nexus:1760" + }, + + "Seasonal Immersion": { + "ID": "Entoarox.SeasonalImmersion", + "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + "~1.11 | UpdateKey": "Chucklefish:4262" // only enable update checks up to 1.11 by request (has its own update-check feature) + }, + + "Seed Bag": { + "ID": "Platonymous.SeedBag", + "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 + }, + + "Seed Catalogue": { + "ID": "spacechase0.SeedCatalogue", + "Default | UpdateKey": "Nexus:1640" + }, + + "Self Service": { + "ID": "JarvieK.SelfService", + "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated + "Default | UpdateKey": "Nexus:1304" + }, + + "Send Items": { + "ID": "Denifia.SendItems", + "Default | UpdateKey": "Nexus:1087" // added in 1.0.3 (2017-10-04) + }, + + "Shed Notifications (BuildingsNotifications)": { + "ID": "TheCroak.BuildingsNotifications", + "Default | UpdateKey": "Nexus:620", + "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shenandoah Project": { + "ID": "Nishtra.ShenandoahProject", + "FormerIDs": "Shenandoah Project", // changed in 1.2 + "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest + "Default | UpdateKey": "Nexus:756" + }, + + "Ship Anywhere": { + "ID": "spacechase0.ShipAnywhere", + "Default | UpdateKey": "Nexus:1379" + }, + + "Shipment Tracker": { + "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", + "Default | UpdateKey": "Nexus:321" + }, + + "Shop Expander": { + "ID": "Entoarox.ShopExpander", + "FormerIDs": "EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + "MapRemoteVersions": { "1.6.0b": "1.6.0" }, + "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature) + }, + + "Showcase Mod": { + "ID": "Igorious.Showcase", + "MapLocalVersions": { "0.9-500": "0.9" }, + "Default | UpdateKey": "Chucklefish:4487", + "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Shroom Spotter": { + "ID": "TehPers.ShroomSpotter", + "Default | UpdateKey": "Nexus:908" + }, + + "Simple Crop Label": { + "ID": "SimpleCropLabel", + "Default | UpdateKey": "Nexus:314" + }, + + "Simple Sound Manager": { + "ID": "Omegasis.SimpleSoundManager", + "Default | UpdateKey": "Nexus:1410" // added in 1.0.1 + }, + + "Simple Sprinklers": { + "ID": "tZed.SimpleSprinkler", + "Default | UpdateKey": "Nexus:76" + }, + + "Siv's Marriage Mod": { + "ID": "6266959802", // official version + "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions + "MapLocalVersions": { "0.0": "1.4" }, + "Default | UpdateKey": "Nexus:366" + }, + + "Skill Prestige": { + "ID": "alphablackwolf.skillPrestige", + "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 + "Default | UpdateKey": "Nexus:569" + }, + + "Skill Prestige: Cooking Adapter": { + "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", + "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 + "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:569" + }, + + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "FormerIDs": "SkipIntro", // changed in 1.4 + "Default | UpdateKey": "Nexus:533", + "~1.7.2 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Skull Cavern Elevator": { + "ID": "SkullCavernElevator", + "Default | UpdateKey": "Nexus:963" + }, + + "Skull Cave Saver": { + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 + "Default | UpdateKey": "Nexus:175", + "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained + }, + + "Sleepy Eye": { + "ID": "spacechase0.SleepyEye", + "Default | UpdateKey": "Nexus:1152" + }, + + "Slower Fence Decay": { + "ID": "Speeder.SlowerFenceDecay", + "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update + "Default | UpdateKey": "Nexus:252", + "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Smart Mod": { + "ID": "KuroBear.SmartMod", + "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Solar Eclipse Event": { + "ID": "KoihimeNakamura.SolarEclipseEvent", + "Default | UpdateKey": "Nexus:897", + "MapLocalVersions": { "1.3.1-20180131": "1.3.1" } + }, + + "SpaceCore": { + "ID": "spacechase0.SpaceCore", + "Default | UpdateKey": "Nexus:1348" + }, + + "Speedster": { + "ID": "Platonymous.Speedster", + "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 + }, + + "Split Screen": { + "ID": "Ilyaki.SplitScreen", + "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + + "Sprinkler Range": { + "ID": "cat.sprinklerrange", + "Default | UpdateKey": "Nexus:1179" + }, + + "Sprinkles": { + "ID": "Platonymous.Sprinkles", + "Default | UpdateKey": "Chucklefish:4592" + }, + + "Sprint and Dash": { + "ID": "SPDSprintAndDash", + "Default | UpdateKey": "Nexus:235", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Sprint and Dash Redux": { + "ID": "littleraskol.SprintAndDashRedux", + "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 + "Default | UpdateKey": "Chucklefish:4201" + }, + + "StackSplitX": { + "ID": "tstaples.StackSplitX", + "Default | UpdateKey": "Nexus:798" + }, + + "Stardew Config Menu": { + "ID": "Juice805.StardewConfigMenu", + "Default | UpdateKey": "Nexus:1312" + }, + + "Stardew Content Compatibility Layer (SCCL)": { + "ID": "SCCL", + "Default | UpdateKey": "Nexus:889", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Stardew Editor Game Integration": { + "ID": "spacechase0.StardewEditor.GameIntegration", + "Default | UpdateKey": "Nexus:1298" + }, + + "Stardew Notification": { + "ID": "stardewnotification", + "Default | UpdateKey": "GitHub:monopandora/StardewNotification" + }, + + "Stardew Symphony": { + "ID": "Omegasis.StardewSymphony", + "Default | UpdateKey": "Nexus:425" // added in 1.4.1 + }, + + "StarDustCore": { + "ID": "StarDustCore", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, + + "Starting Money": { + "ID": "mmanlapat.StartingMoney", + "FormerIDs": "StartingMoney", // changed in 1.1 + "Default | UpdateKey": "Nexus:1138" + }, + + "StashItemsToChest": { + "ID": "BlueMod_StashItemsToChest", + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stephan's Lots of Crops": { + "ID": "stephansstardewcrops", + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "Default | UpdateKey": "Chucklefish:4314" + }, + + "Stumps to Hardwood Stumps": { + "ID": "StumpsToHardwoodStumps", + "Default | UpdateKey": "Nexus:691" + }, + + "Summit Reborn": { + "ID": "KoihimeNakamura.summitreborn", + "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) + }, + + "Super Greenhouse Warp Modifier": { + "ID": "SuperGreenhouse", + "Default | UpdateKey": "Chucklefish:4334", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Swim Almost Anywhere / Swim Suit": { + "ID": "Platonymous.SwimSuit", + "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 + }, + + "Tapper Ready": { + "ID": "skunkkk.TapperReady", + "Default | UpdateKey": "Nexus:1219" + }, + + "Teh's Fishing Overhaul": { + "ID": "TehPers.FishingOverhaul", + "Default | UpdateKey": "Nexus:866" + }, + + "Teleporter": { + "ID": "Teleporter", + "Default | UpdateKey": "Chucklefish:4374", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "The Long Night": { + "ID": "Pathoschild.TheLongNight", + "Default | UpdateKey": "Nexus:1369", + "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Three-heart Dance Partner": { + "ID": "ThreeHeartDancePartner", + "Default | UpdateKey": "Nexus:500" + }, + + "TimeFreeze": { + "ID": "Omegasis.TimeFreeze", + "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 + "Default | UpdateKey": "Nexus:973" // added in 1.2.1 + }, + + "Time Reminder": { + "ID": "KoihimeNakamura.TimeReminder", + "MapLocalVersions": { "1.0-20170314": "1.0.2" }, + "Default | UpdateKey": "Nexus:1000" + }, + + "TimeSpeed": { + "ID": "cantorsdust.TimeSpeed", + "FormerIDs": "community.TimeSpeed", // changed in 2.3.3 + "Default | UpdateKey": "Nexus:169" + }, + + "To Do List": { + "ID": "eleanor.todolist", + "Default | UpdateKey": "Nexus:1630" + }, + + "Tool Charging": { + "ID": "mralbobo.ToolCharging", + "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" + }, + + "TractorMod": { + "ID": "Pathoschild.TractorMod", + "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "Default | UpdateKey": "Nexus:1401", + "~4.5-beta | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "TrainerMod": { + "ID": "SMAPI.TrainerMod", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." + }, + + "Tree Transplant": { + "ID": "TreeTransplant", + "Default | UpdateKey": "Nexus:1342" + }, + + "UI Info Suite": { + "ID": "Cdaragorn.UiInfoSuite", + "Default | UpdateKey": "Nexus:1150" + }, + + "UiModSuite": { + "ID": "Demiacle.UiModSuite", + "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest + "Default | UpdateKey": "Nexus:1023", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Variable Grass": { + "ID": "dantheman999.VariableGrass", + "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" + }, + + "Vertical Toolbar": { + "ID": "SB_VerticalToolMenu", + "Default | UpdateKey": "Nexus:943" + }, + + "WarpAnimals": { + "ID": "Symen.WarpAnimals", + "Default | UpdateKey": "Nexus:1400" + }, + + "What Farm Cave / WhatAMush": { + "ID": "WhatAMush", + "Default | UpdateKey": "Nexus:1097" + }, + + "WHats Up": { + "ID": "wHatsUp", + "Default | UpdateKey": "Nexus:1082" + }, + + "Winter Grass": { + "ID": "cat.wintergrass", + "Default | UpdateKey": "Nexus:1601" + }, + + "Xnb Loader": { + "ID": "Entoarox.XnbLoader", + "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature) + }, + + "zDailyIncrease": { + "ID": "zdailyincrease", + "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest + "Default | UpdateKey": "Chucklefish:4247" + }, + + "Zoom Out Extreme": { + "ID": "RockinMods.ZoomMod", + "FormerIDs": "ZoomMod", // changed circa 1.2.1 + "Default | UpdateKey": "Nexus:1326", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Better RNG": { + "ID": "Zoryn.BetterRNG", + "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Calendar Anywhere": { + "ID": "Zoryn.CalendarAnywhere", + "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Durable Fences": { + "ID": "Zoryn.DurableFences", + "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Health Bars": { + "ID": "Zoryn.HealthBars", + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Fishing Mod": { + "ID": "Zoryn.FishingMod", + "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Junimo Deposit Anywhere": { + "ID": "Zoryn.JunimoDepositAnywhere", + "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Movement Mod": { + "ID": "Zoryn.MovementModifier", + "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Regen Mod": { + "ID": "Zoryn.RegenMod", + "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f849ee53..c13f5e30 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -293,16 +293,17 @@ Designer - Always + PreserveNewest - + + StardewModdingAPI.metadata.json PreserveNewest - Always + PreserveNewest diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json deleted file mode 100644 index 343257f1..00000000 --- a/src/SMAPI/StardewModdingAPI.metadata.json +++ /dev/null @@ -1,1668 +0,0 @@ -{ - /** - * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This - * field shouldn't be edited by players in most cases. - * - * Standard fields - * =============== - * The predefined fields are documented below (only 'ID' is required). Each entry's key is the - * default display name for the mod if one isn't available (e.g. in dependency checks). - * - * - ID: the mod's latest unique ID (if any). - * - * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching - * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple - * variants can be separated with '|'. - * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * - * Versioned metadata - * ================== - * Each record can also specify extra metadata using the field keys below. - * - * Each key consists of a field name prefixed with any combination of version range and 'Default', - * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, - * 'Default | UpdateKey' will only override if the mod has no update keys, and - * '~1.1 | Default | Name' will do the same up to version 1.1. - * - * The version format is 'min~max' (where either side can be blank for unbounded), or a single - * version number. - * - * These are the valid field names: - * - * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update - * checks for older mods that haven't been updated to use it yet. - * - * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load - * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it - * even if it detects incompatible code). - * - * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility - * automatically, since that hides the details from trace logs. - * - * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded - * (if applicable). If blank, will default to a generic not-compatible message. - * - * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the - * mod is no longer compatible. - */ - "ModData": { - "AccessChestAnywhere": { - "ID": "AccessChestAnywhere", - "MapLocalVersions": { "1.1-1078": "1.1" }, - "Default | UpdateKey": "Nexus:257", - "~1.1 | Status": "AssumeBroken" - }, - - "Adjust Artisan Prices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" }, - "Default | UpdateKey": "Chucklefish:3532" - }, - - "Adjust Monster": { - "ID": "mmanlapat.AdjustMonster", - "Default | UpdateKey": "Nexus:1161" - }, - - "Advanced Location Loader": { - "ID": "Entoarox.AdvancedLocationLoader", - "~1.3.7 | UpdateKey": "Chucklefish:3619" // only enable update checks up to 1.3.7 by request (has its own update-check feature) - }, - - "Adventure Shop Inventory": { - "ID": "HammurabiAdventureShopInventory", - "Default | UpdateKey": "Chucklefish:4608" - }, - - "AgingMod": { - "ID": "skn.AgingMod", - "Default | UpdateKey": "Nexus:1129", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "All Crops All Seasons": { - "ID": "cantorsdust.AllCropsAllSeasons", - "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 - "Default | UpdateKey": "Nexus:170" - }, - - "All Professions": { - "ID": "cantorsdust.AllProfessions", - "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:174" - }, - - "Almighty Farming Tool": { - "ID": "439", - "MapRemoteVersions": { "1.21": "1.2.1" }, - "Default | UpdateKey": "Nexus:439" - }, - - "Animal Husbandry": { - "ID": "DIGUS.ANIMALHUSBANDRYMOD", - "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 - "Default | UpdateKey": "Nexus:1538" - }, - - "Animal Mood Fix": { - "ID": "GPeters-AnimalMoodFix", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - - "Animal Sitter": { - "ID": "jwdred.AnimalSitter", - "Default | UpdateKey": "Nexus:581", - "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Arcade Pong": { - "ID": "Platonymous.ArcadePong", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - - "A Tapper's Dream": { - "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "Default | UpdateKey": "Nexus:260" - }, - - "Auto Animal Doors": { - "ID": "AaronTaggart.AutoAnimalDoors", - "Default | UpdateKey": "Nexus:1019" - }, - - "Auto-Eat": { - "ID": "Permamiss.AutoEat", - "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 - "Default | UpdateKey": "Nexus:643" - }, - - "AutoFish": { - "ID": "WhiteMind.AF", - "Default | UpdateKey": "Nexus:1895" - }, - - "AutoGate": { - "ID": "AutoGate", - "Default | UpdateKey": "Nexus:820" - }, - - "Automate": { - "ID": "Pathoschild.Automate", - "Default | UpdateKey": "Nexus:1063", - "~1.10-beta.7 | Status": "AssumeBroken" // broke in SDV 1.3.20 - }, - - "Automated Doors": { - "ID": "azah.automated-doors", - "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 - "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 - }, - - "AutoSpeed": { - "ID": "Omegasis.AutoSpeed", - "Default | UpdateKey": "Nexus:443" // added in 1.4.1 - }, - - "Basic Sprinklers Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:833" - }, - - "Better Hay": { - "ID": "cat.betterhay", - "Default | UpdateKey": "Nexus:1430" - }, - - "Better Quality More Seasons": { - "ID": "SB_BQMS", - "Default | UpdateKey": "Nexus:935" - }, - - "Better Quarry": { - "ID": "BetterQuarry", - "Default | UpdateKey": "Nexus:771" - }, - - "Better Ranching": { - "ID": "BetterRanching", - "Default | UpdateKey": "Nexus:859" - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" }, - "Default | UpdateKey": "Chucklefish:4302" - }, - - "Better Sprinklers": { - "ID": "Speeder.BetterSprinklers", - "FormerIDs": "SPDSprinklersMod", // changed in 2.3 - "Default | UpdateKey": "Nexus:41" - }, - - "Billboard Anywhere": { - "ID": "Omegasis.BillboardAnywhere", - "Default | UpdateKey": "Nexus:492" // added in 1.4.1 - }, - - "Birthday Mail": { - "ID": "KathrynHazuka.BirthdayMail", - "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update - "Default | UpdateKey": "Nexus:276", - "MapRemoteVersions": { "1.3.1": "1.3" } // manifest not updated - }, - - "Breed Like Rabbits": { - "ID": "dycedarger.breedlikerabbits", - "Default | UpdateKey": "Nexus:948" - }, - - "Build Endurance": { - "ID": "Omegasis.BuildEndurance", - "Default | UpdateKey": "Nexus:445" // added in 1.4.1 - }, - - "Build Health": { - "ID": "Omegasis.BuildHealth", - "Default | UpdateKey": "Nexus:446" // added in 1.4.1 - }, - - "Buy Cooking Recipes": { - "ID": "Denifia.BuyRecipes", - "Default | UpdateKey": "Nexus:1126" // added in 1.0.1 (2017-10-04) - }, - - "Buy Back Collectables": { - "ID": "Omegasis.BuyBackCollectables", - "FormerIDs": "BuyBackCollectables", // changed in 1.4 - "Default | UpdateKey": "Nexus:507" // added in 1.4.1 - }, - - "Carry Chest": { - "ID": "spacechase0.CarryChest", - "Default | UpdateKey": "Nexus:1333" - }, - - "Casks Anywhere": { - "ID": "CasksAnywhere", - "MapLocalVersions": { "1.1-alpha": "1.1" }, - "Default | UpdateKey": "Nexus:878" - }, - - "Categorize Chests": { - "ID": "CategorizeChests", - "Default | UpdateKey": "Nexus:1300", - "~1.4.3-unofficial.2.mizzion | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" }, - "Default | UpdateKey": "Nexus:1030" - }, - - "Chest Label System": { - "ID": "Speeder.ChestLabel", - "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update - "Default | UpdateKey": "Nexus:242" - }, - - "Chest Pooling": { - "ID": "mralbobo.ChestPooling", - "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling" - }, - - "Chests Anywhere": { - "ID": "Pathoschild.ChestsAnywhere", - "FormerIDs": "ChestsAnywhere", // changed in 1.9 - "Default | UpdateKey": "Nexus:518", - "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "CJB Automation": { - "ID": "CJBAutomation", - "Default | UpdateKey": "Nexus:211", - "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" - }, - - "CJB Cheats Menu": { - "ID": "CJBok.CheatsMenu", - "FormerIDs": "CJBCheatsMenu", // changed in 1.14 - "Default | UpdateKey": "Nexus:4", - "~1.18-beta | Status": "AssumeBroken" // broke in SDV 1.3, first beta causes significant friendship bugs - }, - - "CJB Item Spawner": { - "ID": "CJBok.ItemSpawner", - "FormerIDs": "CJBItemSpawner", // changed in 1.7 - "Default | UpdateKey": "Nexus:93", - "~1.10 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "CJB Show Item Sell Price": { - "ID": "CJBok.ShowItemSellPrice", - "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 - "Default | UpdateKey": "Nexus:5", - "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Clean Farm": { - "ID": "tstaples.CleanFarm", - "Default | UpdateKey": "Nexus:794" - }, - - "Climates of Ferngill": { - "ID": "KoihimeNakamura.ClimatesOfFerngill", - "Default | UpdateKey": "Nexus:604" - }, - - "Coal Regen": { - "ID": "Blucifer.CoalRegen", - "Default | UpdateKey": "Nexus:1664" - }, - - "Cobalt": { - "ID": "spacechase0.Cobalt", - "MapRemoteVersions": { "1.1.3": "1.1.2" } // not updated in manifest - }, - - "Cold Weather Haley": { - "ID": "LordXamon.ColdWeatherHaleyPRO", - "Default | UpdateKey": "Nexus:1169", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Colored Chests": { - "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - - "Combat with Farm Implements": { - "ID": "SPDFarmingImplementsInCombat", - "Default | UpdateKey": "Nexus:313" - }, - - "Community Bundle Item Tooltip": { - "ID": "musbah.bundleTooltip", - "Default | UpdateKey": "Nexus:1329" - }, - - "Concentration on Farming": { - "ID": "punyo.ConcentrationOnFarming", - "Default | UpdateKey": "Nexus:1445" - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" }, - "Default | UpdateKey": "Nexus:280" - }, - - "Configurable Shipping Dates": { - "ID": "ConfigurableShippingDates", - "Default | UpdateKey": "Nexus:675", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Content Patcher": { - "ID": "Pathoschild.ContentPatcher", - "Default | UpdateKey": "Nexus:1915", - "~1.4-beta.5 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors) - }, - - "Cooking Skill": { - "ID": "spacechase0.CookingSkill", - "FormerIDs": "CookingSkill", // changed in 1.0.4–6 - "Default | UpdateKey": "Nexus:522" - }, - - "CrabNet": { - "ID": "jwdred.CrabNet", - "Default | UpdateKey": "Nexus:584" - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "Default | UpdateKey": "Nexus:1585", - "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest - }, - - "Current Location": { - "ID": "CurrentLocation102120161203", - "Default | UpdateKey": "Nexus:638" - }, - - "Custom Asset Modifier": { - "ID": "Omegasis.CustomAssetModifier", - "Default | UpdateKey": "1836" - }, - - "Custom Critters": { - "ID": "spacechase0.CustomCritters", - "Default | UpdateKey": "Nexus:1255" - }, - - "Custom Crops": { - "ID": "spacechase0.CustomCrops", - "Default | UpdateKey": "Nexus:1592" - }, - - "Custom Element Handler": { - "ID": "Platonymous.CustomElementHandler", - "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 - }, - - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991" // added in 0.6.1 - }, - - "Custom Farming Automate Bridge": { - "ID": "Platonymous.CFAutomate", - "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate - "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" - }, - - "Custom Farm Types": { - "ID": "spacechase0.CustomFarmTypes", - "Default | UpdateKey": "Nexus:1140" - }, - - "Custom Furniture": { - "ID": "Platonymous.CustomFurniture", - "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 - }, - - "Customize Exterior": { - "ID": "spacechase0.CustomizeExterior", - "FormerIDs": "CustomizeExterior", // changed in 1.0.3 - "Default | UpdateKey": "Nexus:1099", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Customizable Cart Redux": { - "ID": "KoihimeNakamura.CCR", - "MapLocalVersions": { "1.1-20170917": "1.1" }, - "Default | UpdateKey": "Nexus:1402" - }, - - "Customizable Traveling Cart Days": { - "ID": "TravelingCartYyeahdude", - "Default | UpdateKey": "Nexus:567" - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1027" - }, - - "Custom NPC": { - "ID": "Platonymous.CustomNPC", - "Default | UpdateKey": "Nexus:1607" - }, - - "Custom Shops Redux": { - "ID": "Omegasis.CustomShopReduxGui", - "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 - }, - - "Custom TV": { - "ID": "Platonymous.CustomTV", - "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 - }, - - "Daily Luck Message": { - "ID": "Schematix.DailyLuckMessage", - "Default | UpdateKey": "Nexus:1327" - }, - - "Daily News": { - "ID": "bashNinja.DailyNews", - "Default | UpdateKey": "Nexus:1141", - "~1.2 | Status": "AssumeBroken" // broke in Stardew Valley 1.3 (or depends on CustomTV which broke) - }, - - "Daily Quest Anywhere": { - "ID": "Omegasis.DailyQuestAnywhere", - "FormerIDs": "DailyQuest", // changed in 1.4 - "Default | UpdateKey": "Nexus:513" // added in 1.4.1 - }, - - "Data Maps": { - "ID": "Pathoschild.DataMaps", - "Default | UpdateKey": "Nexus:1691", - "~1.3 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Debug Mode": { - "ID": "Pathoschild.DebugMode", - "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 - "Default | UpdateKey": "Nexus:679", - "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Did You Water Your Crops?": { - "ID": "Nishtra.DidYouWaterYourCrops", - "Default | UpdateKey": "Nexus:1583" - }, - - "Dynamic Checklist": { - "ID": "gunnargolf.DynamicChecklist", - "Default | UpdateKey": "Nexus:1145" // added in 1.0.1-pathoschild-update - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated - "Default | UpdateKey": "Nexus:874" - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" }, - "Default | UpdateKey": "Nexus:374", - "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Dynamic NPC Sprites": { - "ID": "BashNinja.DynamicNPCSprites", - "Default | UpdateKey": "Nexus:1183" - }, - - "Easier Farming": { - "ID": "cautiouswafffle.EasierFarming", - "Default | UpdateKey": "Nexus:1426" - }, - - "Empty Hands": { - "ID": "QuicksilverFox.EmptyHands", - "Default | UpdateKey": "Nexus:1176" // added in 1.0.1-pathoschild-update - }, - - "Enemy Health Bars": { - "ID": "Speeder.HealthBars", - "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update - "Default | UpdateKey": "Nexus:193" - }, - - "Entoarox Framework": { - "ID": "Entoarox.EntoaroxFramework", - "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? - "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) - "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) - }, - - "Expanded Fridge": { - "ID": "Uwazouri.ExpandedFridge", - "Default | UpdateKey": "Nexus:1191" - }, - - "Experience Bars": { - "ID": "spacechase0.ExperienceBars", - "FormerIDs": "ExperienceBars", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:509" - }, - - "Extended Bus System": { - "ID": "ExtendedBusSystem", - "Default | UpdateKey": "Chucklefish:4373" - }, - - "Extended Fridge": { - "ID": "Crystalmir.ExtendedFridge", - "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 - "Default | UpdateKey": "Nexus:485" - }, - - "Extended Greenhouse": { - "ID": "ExtendedGreenhouse", - "Default | UpdateKey": "Chucklefish:4303", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Extended Minecart": { - "ID": "Entoarox.ExtendedMinecart", - "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) - }, - - "Extended Reach": { - "ID": "spacechase0.ExtendedReach", - "Default | UpdateKey": "Nexus:1493" - }, - - "Fall 28 Snow Day": { - "ID": "Omegasis.Fall28SnowDay", - "Default | UpdateKey": "Nexus:486", // added in 1.4.1 - "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything - }, - - "Farm Automation Unofficial: Item Collector": { - "ID": "Maddy99.FarmAutomation.ItemCollector", - "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Farm Expansion": { - "ID": "Advize.FarmExpansion", - "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 - "Default | UpdateKey": "Nexus:130" - }, - - "Fast Animations": { - "ID": "Pathoschild.FastAnimations", - "Default | UpdateKey": "Nexus:1089", - "~1.5 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Faster Grass": { - "ID": "IceGladiador.FasterGrass", - "Default | UpdateKey": "Nexus:1772" - }, - - "Faster Paths": { - "ID": "Entoarox.FasterPaths", - "FormerIDs": "615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.3 - "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) - }, - - "Fishing Adjust": { - "ID": "shuaiz.FishingAdjustMod", - "Default | UpdateKey": "Nexus:1350", - "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' - }, - - "Fishing Tuner Redux": { - "ID": "HammurabiFishingTunerRedux", - "Default | UpdateKey": "Chucklefish:4578" - }, - - "Fixed Secret Woods Debris": { - "ID": "f4iTh.WoodsDebrisFix", - "Default | UpdateKey": "Nexus:1941" - }, - - "Flower Color Picker": { - "ID": "spacechase0.FlowerColorPicker", - "Default | UpdateKey": "Nexus:1229" - }, - - "Forage at the Farm": { - "ID": "Nishtra.ForageAtTheFarm", - "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 - "Default | UpdateKey": "Nexus:673" - }, - - "Furniture Anywhere": { - "ID": "Entoarox.FurnitureAnywhere", - "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) - }, - - "Game Reminder": { - "ID": "mmanlapat.GameReminder", - "Default | UpdateKey": "Nexus:1153" - }, - - "Gate Opener": { - "ID": "mralbobo.GateOpener", - "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener" - }, - - "GenericShopExtender": { - "ID": "GenericShopExtender", - "Default | UpdateKey": "Nexus:814" // added in 0.1.3 - }, - - "Geode Info Menu": { - "ID": "cat.geodeinfomenu", - "Default | UpdateKey": "Nexus:1448" - }, - - "Get Dressed": { - "ID": "Advize.GetDressed", - "Default | UpdateKey": "Nexus:331" - }, - - "Giant Crop Ring": { - "ID": "cat.giantcropring", - "Default | UpdateKey": "Nexus:1182" - }, - - "Gift Taste Helper": { - "ID": "tstaples.GiftTasteHelper", - "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 - "Default | UpdateKey": "Nexus:229" - }, - - "Grandfather's Gift": { - "ID": "ShadowDragon.GrandfathersGift", - "Default | UpdateKey": "Nexus:985" - }, - - "Happy Animals": { - "ID": "HappyAnimals", - "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Happy Birthday (Omegasis)": { - "ID": "Omegasis.HappyBirthday", - "Default | UpdateKey": "Nexus:520" // added in 1.4.1 - }, - - "Hardcore Mines": { - "ID": "kibbe.hardcore_mines", - "Default | UpdateKey": "Nexus:1674" - }, - - "Harp of Yoba Redux": { - "ID": "Platonymous.HarpOfYobaRedux", - "Default | UpdateKey": "Nexus:914" // added in 2.0.3 - }, - - "Harvest Moon Witch Princess": { - "ID": "Sasara.WitchPrincess", - "Default | UpdateKey": "Nexus:1157" - }, - - "Harvest With Scythe": { - "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "Default | UpdateKey": "Nexus:236" - }, - - "Horse Whistle (icepuente)": { - "ID": "icepuente.HorseWhistle", - "Default | UpdateKey": "Nexus:1131", - "~1.1.2-unofficial.1-pathoschild | Status": "AssumeBroken" // causes significant lag, fixed in unofficial.2 - }, - - "Hunger (Yyeadude)": { - "ID": "HungerYyeadude", - "Default | UpdateKey": "Nexus:613" - }, - - "Hunger for Food (Tigerle)": { - "ID": "HungerForFoodByTigerle", - "Default | UpdateKey": "Nexus:810", - "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Hunger Mod (skn)": { - "ID": "skn.HungerMod", - "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1127" - }, - - "Idle Pause": { - "ID": "Veleek.IdlePause", - "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:1092" - }, - - "Improved Quality of Life": { - "ID": "Demiacle.ImprovedQualityOfLife", - "Default | UpdateKey": "Nexus:1025", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Instant Geode": { - "ID": "InstantGeode", - "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Instant Grow Trees": { - "ID": "cantorsdust.InstantGrowTrees", - "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 - "Default | UpdateKey": "Nexus:173" - }, - - "Interaction Helper": { - "ID": "HammurabiInteractionHelper", - "Default | UpdateKey": "Chucklefish:4640" // added in 1.0.4-pathoschild-update - }, - - "Item Auto Stacker": { - "ID": "cat.autostacker", - "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated - "Default | UpdateKey": "Nexus:1184" - }, - - "Json Assets": { - "ID": "spacechase0.JsonAssets", - "Default | UpdateKey": "Nexus:1720" - }, - - "Junimo Farm": { - "ID": "Platonymous.JunimoFarm", - "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:984" // added in 1.1.3 - }, - - "Less Strict Over-Exertion (AntiExhaustion)": { - "ID": "BALANCEMOD_AntiExhaustion", - "MapLocalVersions": { "0.0": "1.1" }, - "Default | UpdateKey": "Nexus:637", - "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Level Extender": { - "ID": "DevinLematty.LevelExtender", - "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3 - "Default | UpdateKey": "Nexus:1471" - }, - - "Level Up Notifications": { - "ID": "Level Up Notifications", - "MapRemoteVersions": { "0.0.1a": "0.0.1" }, - "Default | UpdateKey": "Nexus:855" - }, - - "Location and Music Logging": { - "ID": "Brandy Lover.LMlog", - "Default | UpdateKey": "Nexus:1366" - }, - - "Longevity": { - "ID": "RTGOAT.Longevity", - "MapRemoteVersions": { "1.6.8h": "1.6.8" }, - "Default | UpdateKey": "Nexus:649" - }, - - "Lookup Anything": { - "ID": "Pathoschild.LookupAnything", - "FormerIDs": "LookupAnything", // changed in 1.10.1 - "Default | UpdateKey": "Nexus:541", - "~1.18.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Love Bubbles": { - "ID": "LoveBubbles", - "Default | UpdateKey": "Nexus:1318" - }, - - "Loved Labels": { - "ID": "Advize.LovedLabels", - "Default | UpdateKey": "Nexus:279" - }, - - "Luck Skill": { - "ID": "spacechase0.LuckSkill", - "FormerIDs": "LuckSkill", // changed in 0.1.4 - "Default | UpdateKey": "Nexus:521" - }, - - "Magic": { - "ID": "spacechase0.Magic", - "MapRemoteVersions": { "0.1.2": "0.1.1" } // not updated in manifest - }, - - "Mail Framework": { - "ID": "DIGUS.MailFrameworkMod", - "Default | UpdateKey": "Nexus:1536" - }, - - "MailOrderPigs": { - "ID": "jwdred.MailOrderPigs", - "Default | UpdateKey": "Nexus:632" - }, - - "Makeshift Multiplayer": { - "ID": "spacechase0.StardewValleyMP", - "FormerIDs": "StardewValleyMP", // changed in 0.3 - "Default | UpdateKey": "Nexus:501" - }, - - "Map Image Exporter": { - "ID": "spacechase0.MapImageExporter", - "FormerIDs": "MapImageExporter", // changed in 1.0.2 - "Default | UpdateKey": "Nexus:1073" - }, - - "Message Box [API]? (ChatMod)": { - "ID": "Kithio:ChatMod", - "Default | UpdateKey": "Chucklefish:4296", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Mining at the Farm": { - "ID": "Nishtra.MiningAtTheFarm", - "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 - "Default | UpdateKey": "Nexus:674" - }, - - "Mining With Explosives": { - "ID": "Nishtra.MiningWithExplosives", - "FormerIDs": "MiningWithExplosives", // changed in 1.1 - "Default | UpdateKey": "Nexus:770" - }, - - "Modder Serialization Utility": { - "ID": "SerializerUtils-0-1", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "Monster Level Tip": { - "ID": "WhiteMind.MonsterLT", - "Default | UpdateKey": "Nexus:1896" - }, - - "More Animals": { - "ID": "Entoarox.MoreAnimals", - "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 - "~2.0.2 | UpdateKey": "Chucklefish:4288" // only enable update checks up to 2.0.2 by request (has its own update-check feature) - }, - - "More Artifact Spots": { - "ID": "451", - "Default | UpdateKey": "Nexus:451" - }, - - "More Map Layers": { - "ID": "Platonymous.MoreMapLayers", - "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 - }, - - "More Rain": { - "ID": "Omegasis.MoreRain", - "Default | UpdateKey": "Nexus:441", // added in 1.5.1 - "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "More Weapons": { - "ID": "Joco80.MoreWeapons", - "Default | UpdateKey": "Nexus:1168" - }, - - "Move Faster": { - "ID": "shuaiz.MoveFasterMod", - "Default | UpdateKey": "Nexus:1351", - "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" }, - "Default | UpdateKey": "Nexus:1094", - "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Museum Rearranger": { - "ID": "Omegasis.MuseumRearranger", - "Default | UpdateKey": "Nexus:428" // added in 1.4.1 - }, - - "Mushroom Level Tip": { - "ID": "WhiteMind.MLT", - "Default | UpdateKey": "Nexus:1894" - }, - - "New Machines": { - "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "Default | UpdateKey": "Chucklefish:3683", - "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Night Owl": { - "ID": "Omegasis.NightOwl", - "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest - "Default | UpdateKey": "Nexus:433" // added in 1.4.1 - }, - - "No Crows": { - "ID": "cat.nocrows", - "Default | UpdateKey": "Nexus:1682" - }, - - "No Kids Ever": { - "ID": "Hangy.NoKidsEver", - "Default | UpdateKey": "Nexus:1464" - }, - - "No Debug Mode": { - "ID": "NoDebugMode", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - - "No Fence Decay": { - "ID": "cat.nofencedecay", - "Default | UpdateKey": "Nexus:1180" - }, - - "No More Pets": { - "ID": "Omegasis.NoMorePets", - "FormerIDs": "NoMorePets", // changed in 1.4 - "Default | UpdateKey": "Nexus:506" // added in 1.4.1 - }, - - "No Rumble Horse": { - "ID": "Xangria.NoRumbleHorse", - "Default | UpdateKey": "Nexus:1779" - }, - - "No Soil Decay": { - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "Default | UpdateKey": "Nexus:237", - "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location - }, - - "No Soil Decay Redux": { - "ID": "Platonymous.NoSoilDecayRedux", - "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 - }, - - "NPC Map Locations": { - "ID": "Bouhm.NPCMapLocations", - "FormerIDs": "NPCMapLocationsMod", // changed in 2.0 - "Default | UpdateKey": "Nexus:239" - }, - - "Object Time Left": { - "ID": "spacechase0.ObjectTimeLeft", - "Default | UpdateKey": "Nexus:1315" - }, - - "OmniFarm": { - "ID": "PhthaloBlue.OmniFarm", - "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm" - }, - - "One Click Shed": { - "ID": "BitwiseJonMods.OneClickShedReloader", - "Default | UpdateKey": "Nexus:2052" - }, - - "Out of Season Bonuses (Seasonal Items)": { - "ID": "midoriarmstrong.seasonalitems", - "Default | UpdateKey": "Nexus:1452" - }, - - "Part of the Community": { - "ID": "SB_PotC", - "Default | UpdateKey": "Nexus:923" - }, - - "PelicanFiber": { - "ID": "jwdred.PelicanFiber", - "Default | UpdateKey": "Nexus:631" - }, - - "PelicanTTS": { - "ID": "Platonymous.PelicanTTS", - "Default | UpdateKey": "Nexus:1079" // added in 1.6.1 - }, - - "Persia the Mermaid - Standalone Custom NPC": { - "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "Default | UpdateKey": "Nexus:1419" - }, - - "Persistent Game Options": { - "ID": "Xangria.PersistentGameOptions", - "Default | UpdateKey": "Nexus:1778" - }, - - "Plant on Grass": { - "ID": "Demiacle.PlantOnGrass", - "Default | UpdateKey": "Nexus:1026" - }, - - "PyTK - Platonymous Toolkit": { - "ID": "Platonymous.Toolkit", - "Default | UpdateKey": "Nexus:1726" - }, - - "Point-and-Plant": { - "ID": "jwdred.PointAndPlant", - "Default | UpdateKey": "Nexus:572", - "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated - }, - - "Pony Weight Loss Program": { - "ID": "BadNetCode.PonyWeightLossProgram", - "Default | UpdateKey": "Nexus:1232" - }, - - "Portraiture": { - "ID": "Platonymous.Portraiture", - "Default | UpdateKey": "Nexus:999" // added in 1.3.1 - }, - - "Prairie King Made Easy": { - "ID": "Mucchan.PrairieKingMadeEasy", - "Default | UpdateKey": "Chucklefish:3594", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Purchasable Recipes": { - "ID": "Paracosm.PurchasableRecipes", - "Default | UpdateKey": "Nexus:1722" - }, - - "Quest Delay": { - "ID": "BadNetCode.QuestDelay", - "Default | UpdateKey": "Nexus:1239" - }, - - "Recatch Legendary Fish": { - "ID": "cantorsdust.RecatchLegendaryFish", - "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 - "Default | UpdateKey": "Nexus:172" - }, - - "Regeneration": { - "ID": "HammurabiRegeneration", - "Default | UpdateKey": "Chucklefish:4584" - }, - - "Relationship Bar UI": { - "ID": "RelationshipBar", - "Default | UpdateKey": "Nexus:1009" - }, - - "RelationshipsEnhanced": { - "ID": "relationshipsenhanced", - "Default | UpdateKey": "Chucklefish:4435", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest - "Default | UpdateKey": "Nexus:751", - "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Rented Tools": { - "ID": "JarvieK.RentedTools", - "Default | UpdateKey": "Nexus:1307" - }, - - "Replanter": { - "ID": "jwdred.Replanter", - "Default | UpdateKey": "Nexus:589" - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" }, - "Default | UpdateKey": "Chucklefish:4465" - }, - - "Reseed": { - "ID": "Roc.Reseed", - "Default | UpdateKey": "Nexus:887" - }, - - "Reusable Wallpapers and Floors (Wallpaper Retain)": { - "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "Default | UpdateKey": "Nexus:356", - "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Ring of Fire": { - "ID": "Platonymous.RingOfFire", - "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 - }, - - "Rope Bridge": { - "ID": "RopeBridge", - "Default | UpdateKey": "Nexus:824" - }, - - "Rotate Toolbar": { - "ID": "Pathoschild.RotateToolbar", - "Default | UpdateKey": "Nexus:1100", - "~1.2.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Rush Orders": { - "ID": "spacechase0.RushOrders", - "FormerIDs": "RushOrders", // changed in 1.1 - "Default | UpdateKey": "Nexus:605" - }, - - "Save Anywhere": { - "ID": "Omegasis.SaveAnywhere", - "Default | UpdateKey": "Nexus:444", // added in 2.6.1 - "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest - }, - - "Save Backup": { - "ID": "Omegasis.SaveBackup", - "Default | UpdateKey": "Nexus:435", // added in 1.3.1 - "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Scroll to Blank": { - "ID": "caraxian.scroll.to.blank", - "Default | UpdateKey": "Chucklefish:4405" - }, - - "Scythe Harvesting": { - "ID": "mmanlapat.ScytheHarvesting", - "FormerIDs": "ScytheHarvesting", // changed in 1.6 - "Default | UpdateKey": "Nexus:1106" - }, - - "SDV Twitch": { - "ID": "MTD.SDVTwitch", - "Default | UpdateKey": "Nexus:1760" - }, - - "Seasonal Immersion": { - "ID": "Entoarox.SeasonalImmersion", - "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "~1.11 | UpdateKey": "Chucklefish:4262" // only enable update checks up to 1.11 by request (has its own update-check feature) - }, - - "Seed Bag": { - "ID": "Platonymous.SeedBag", - "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 - }, - - "Seed Catalogue": { - "ID": "spacechase0.SeedCatalogue", - "Default | UpdateKey": "Nexus:1640" - }, - - "Self Service": { - "ID": "JarvieK.SelfService", - "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated - "Default | UpdateKey": "Nexus:1304" - }, - - "Send Items": { - "ID": "Denifia.SendItems", - "Default | UpdateKey": "Nexus:1087" // added in 1.0.3 (2017-10-04) - }, - - "Shed Notifications (BuildingsNotifications)": { - "ID": "TheCroak.BuildingsNotifications", - "Default | UpdateKey": "Nexus:620", - "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shenandoah Project": { - "ID": "Nishtra.ShenandoahProject", - "FormerIDs": "Shenandoah Project", // changed in 1.2 - "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest - "Default | UpdateKey": "Nexus:756" - }, - - "Ship Anywhere": { - "ID": "spacechase0.ShipAnywhere", - "Default | UpdateKey": "Nexus:1379" - }, - - "Shipment Tracker": { - "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "Default | UpdateKey": "Nexus:321" - }, - - "Shop Expander": { - "ID": "Entoarox.ShopExpander", - "FormerIDs": "EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - "MapRemoteVersions": { "1.6.0b": "1.6.0" }, - "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature) - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" }, - "Default | UpdateKey": "Chucklefish:4487", - "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Shroom Spotter": { - "ID": "TehPers.ShroomSpotter", - "Default | UpdateKey": "Nexus:908" - }, - - "Simple Crop Label": { - "ID": "SimpleCropLabel", - "Default | UpdateKey": "Nexus:314" - }, - - "Simple Sound Manager": { - "ID": "Omegasis.SimpleSoundManager", - "Default | UpdateKey": "Nexus:1410" // added in 1.0.1 - }, - - "Simple Sprinklers": { - "ID": "tZed.SimpleSprinkler", - "Default | UpdateKey": "Nexus:76" - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", // official version - "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions - "MapLocalVersions": { "0.0": "1.4" }, - "Default | UpdateKey": "Nexus:366" - }, - - "Skill Prestige": { - "ID": "alphablackwolf.skillPrestige", - "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 - "Default | UpdateKey": "Nexus:569" - }, - - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated - "Default | UpdateKey": "Nexus:569" - }, - - "Skip Intro": { - "ID": "Pathoschild.SkipIntro", - "FormerIDs": "SkipIntro", // changed in 1.4 - "Default | UpdateKey": "Nexus:533", - "~1.7.2 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Skull Cavern Elevator": { - "ID": "SkullCavernElevator", - "Default | UpdateKey": "Nexus:963" - }, - - "Skull Cave Saver": { - "ID": "cantorsdust.SkullCaveSaver", - "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 - "Default | UpdateKey": "Nexus:175", - "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained - }, - - "Sleepy Eye": { - "ID": "spacechase0.SleepyEye", - "Default | UpdateKey": "Nexus:1152" - }, - - "Slower Fence Decay": { - "ID": "Speeder.SlowerFenceDecay", - "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update - "Default | UpdateKey": "Nexus:252", - "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Smart Mod": { - "ID": "KuroBear.SmartMod", - "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Solar Eclipse Event": { - "ID": "KoihimeNakamura.SolarEclipseEvent", - "Default | UpdateKey": "Nexus:897", - "MapLocalVersions": { "1.3.1-20180131": "1.3.1" } - }, - - "SpaceCore": { - "ID": "spacechase0.SpaceCore", - "Default | UpdateKey": "Nexus:1348" - }, - - "Speedster": { - "ID": "Platonymous.Speedster", - "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 - }, - - "Split Screen": { - "ID": "Ilyaki.SplitScreen", - "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - - "Sprinkler Range": { - "ID": "cat.sprinklerrange", - "Default | UpdateKey": "Nexus:1179" - }, - - "Sprinkles": { - "ID": "Platonymous.Sprinkles", - "Default | UpdateKey": "Chucklefish:4592" - }, - - "Sprint and Dash": { - "ID": "SPDSprintAndDash", - "Default | UpdateKey": "Nexus:235", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Sprint and Dash Redux": { - "ID": "littleraskol.SprintAndDashRedux", - "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 - "Default | UpdateKey": "Chucklefish:4201" - }, - - "StackSplitX": { - "ID": "tstaples.StackSplitX", - "Default | UpdateKey": "Nexus:798" - }, - - "Stardew Config Menu": { - "ID": "Juice805.StardewConfigMenu", - "Default | UpdateKey": "Nexus:1312" - }, - - "Stardew Content Compatibility Layer (SCCL)": { - "ID": "SCCL", - "Default | UpdateKey": "Nexus:889", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Stardew Editor Game Integration": { - "ID": "spacechase0.StardewEditor.GameIntegration", - "Default | UpdateKey": "Nexus:1298" - }, - - "Stardew Notification": { - "ID": "stardewnotification", - "Default | UpdateKey": "GitHub:monopandora/StardewNotification" - }, - - "Stardew Symphony": { - "ID": "Omegasis.StardewSymphony", - "Default | UpdateKey": "Nexus:425" // added in 1.4.1 - }, - - "StarDustCore": { - "ID": "StarDustCore", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - }, - - "Starting Money": { - "ID": "mmanlapat.StartingMoney", - "FormerIDs": "StartingMoney", // changed in 1.1 - "Default | UpdateKey": "Nexus:1138" - }, - - "StashItemsToChest": { - "ID": "BlueMod_StashItemsToChest", - "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", - "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Stephan's Lots of Crops": { - "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated - "Default | UpdateKey": "Chucklefish:4314" - }, - - "Stumps to Hardwood Stumps": { - "ID": "StumpsToHardwoodStumps", - "Default | UpdateKey": "Nexus:691" - }, - - "Summit Reborn": { - "ID": "KoihimeNakamura.summitreborn", - "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) - }, - - "Super Greenhouse Warp Modifier": { - "ID": "SuperGreenhouse", - "Default | UpdateKey": "Chucklefish:4334", - "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 - }, - - "Swim Almost Anywhere / Swim Suit": { - "ID": "Platonymous.SwimSuit", - "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 - }, - - "Tapper Ready": { - "ID": "skunkkk.TapperReady", - "Default | UpdateKey": "Nexus:1219" - }, - - "Teh's Fishing Overhaul": { - "ID": "TehPers.FishingOverhaul", - "Default | UpdateKey": "Nexus:866" - }, - - "Teleporter": { - "ID": "Teleporter", - "Default | UpdateKey": "Chucklefish:4374", - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "The Long Night": { - "ID": "Pathoschild.TheLongNight", - "Default | UpdateKey": "Nexus:1369", - "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Three-heart Dance Partner": { - "ID": "ThreeHeartDancePartner", - "Default | UpdateKey": "Nexus:500" - }, - - "TimeFreeze": { - "ID": "Omegasis.TimeFreeze", - "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 - "Default | UpdateKey": "Nexus:973" // added in 1.2.1 - }, - - "Time Reminder": { - "ID": "KoihimeNakamura.TimeReminder", - "MapLocalVersions": { "1.0-20170314": "1.0.2" }, - "Default | UpdateKey": "Nexus:1000" - }, - - "TimeSpeed": { - "ID": "cantorsdust.TimeSpeed", - "FormerIDs": "community.TimeSpeed", // changed in 2.3.3 - "Default | UpdateKey": "Nexus:169" - }, - - "To Do List": { - "ID": "eleanor.todolist", - "Default | UpdateKey": "Nexus:1630" - }, - - "Tool Charging": { - "ID": "mralbobo.ToolCharging", - "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" - }, - - "TractorMod": { - "ID": "Pathoschild.TractorMod", - "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Default | UpdateKey": "Nexus:1401", - "~4.5-beta | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "TrainerMod": { - "ID": "SMAPI.TrainerMod", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." - }, - - "Tree Transplant": { - "ID": "TreeTransplant", - "Default | UpdateKey": "Nexus:1342" - }, - - "UI Info Suite": { - "ID": "Cdaragorn.UiInfoSuite", - "Default | UpdateKey": "Nexus:1150" - }, - - "UiModSuite": { - "ID": "Demiacle.UiModSuite", - "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest - "Default | UpdateKey": "Nexus:1023", - "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Variable Grass": { - "ID": "dantheman999.VariableGrass", - "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" - }, - - "Vertical Toolbar": { - "ID": "SB_VerticalToolMenu", - "Default | UpdateKey": "Nexus:943" - }, - - "WarpAnimals": { - "ID": "Symen.WarpAnimals", - "Default | UpdateKey": "Nexus:1400" - }, - - "What Farm Cave / WhatAMush": { - "ID": "WhatAMush", - "Default | UpdateKey": "Nexus:1097" - }, - - "WHats Up": { - "ID": "wHatsUp", - "Default | UpdateKey": "Nexus:1082" - }, - - "Winter Grass": { - "ID": "cat.wintergrass", - "Default | UpdateKey": "Nexus:1601" - }, - - "Xnb Loader": { - "ID": "Entoarox.XnbLoader", - "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature) - }, - - "zDailyIncrease": { - "ID": "zdailyincrease", - "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest - "Default | UpdateKey": "Chucklefish:4247" - }, - - "Zoom Out Extreme": { - "ID": "RockinMods.ZoomMod", - "FormerIDs": "ZoomMod", // changed circa 1.2.1 - "Default | UpdateKey": "Nexus:1326", - "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Better RNG": { - "ID": "Zoryn.BetterRNG", - "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Calendar Anywhere": { - "ID": "Zoryn.CalendarAnywhere", - "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Durable Fences": { - "ID": "Zoryn.DurableFences", - "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Health Bars": { - "ID": "Zoryn.HealthBars", - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Fishing Mod": { - "ID": "Zoryn.FishingMod", - "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Junimo Deposit Anywhere": { - "ID": "Zoryn.JunimoDepositAnywhere", - "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 - }, - - "Zoryn's Movement Mod": { - "ID": "Zoryn.MovementModifier", - "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" - }, - - "Zoryn's Regen Mod": { - "ID": "Zoryn.RegenMod", - "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 - "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", - "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 - } - } -} -- cgit From 89ad599561a058289d8ea44e0c345f1a30a3a6ac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 21:41:00 -0400 Subject: add support for writing versions to JSON --- docs/release-notes.md | 1 + src/SMAPI.Web/Startup.cs | 4 ++ .../Converters/SemanticVersionConverter.cs | 60 ++++++++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 062f902e..a8f6d851 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -44,6 +44,7 @@ * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. * Added absolute pixels to `ICursorPosition`. + * Added support for reading/writing `ISemanticVersion` to JSON. * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 58584348..ced6e1c7 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -56,6 +57,9 @@ namespace StardewModdingAPI.Web .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) .AddJsonOptions(options => { + foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) + options.SerializerSettings.Converters.Add(converter); + options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 4f0949fa..9b2f5e7d 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -1,17 +1,67 @@ +using System; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter + internal class SemanticVersionConverter : JsonConverter { /********* - ** Protected methods + ** Accessors + *********/ + /// Get whether this converter can read JSON. + public override bool CanRead => true; + + /// Get whether this converter can write JSON. + public override bool CanWrite => true; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return typeof(ISemanticVersion).IsAssignableFrom(objectType); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader)); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString()); + } + + + /********* + ** Private methods *********/ /// Read a JSON object. /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) + private ISemanticVersion ReadObject(JObject obj) { int major = obj.ValueIgnoreCase("MajorVersion"); int minor = obj.ValueIgnoreCase("MinorVersion"); @@ -26,7 +76,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// Read a JSON string. /// The JSON string value. /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) + private ISemanticVersion ReadString(string str, string path) { if (string.IsNullOrWhiteSpace(str)) return null; -- cgit From 3f5a5e54041a641e30fc5cc899046953d9763da4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 22:01:04 -0400 Subject: use more structured API response for update checks (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 44 +++++++++++---------- .../Framework/ModRepositories/IModRepository.cs | 1 - .../Framework/ModRepositories/ModInfoModel.cs | 2 +- src/SMAPI/Program.cs | 16 +++----- .../Framework/Clients/WebApi/ModEntryModel.cs | 45 +++++++++++++++++++--- .../Clients/WebApi/ModEntryVersionModel.cs | 31 +++++++++++++++ .../ModData/ModDataRecordVersionedFields.cs | 9 +++-- 7 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 960602f4..c4f1023b 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -80,7 +80,11 @@ namespace StardewModdingAPI.Web.Controllers [HttpPost] public async Task> PostAsync([FromBody] ModSearchModel model) { - ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + // parse request data + ISemanticVersion apiVersion = this.GetApiVersion(); + ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray(); + + // perform checks IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) { @@ -119,11 +123,10 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + if (result.Main == null || result.Main.Version.IsOlderThan(version)) { result.Name = data.Name; - result.Url = data.Url; - result.Version = version.ToString(); + result.Main = new ModEntryVersionModel(version, data.Url); } } @@ -136,35 +139,34 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + if (result.Optional == null || result.Optional.Version.IsOlderThan(version)) { result.Name = result.Name ?? data.Name; - result.PreviewUrl = data.Url; - result.PreviewVersion = version.ToString(); + result.Optional = new ModEntryVersionModel(version, data.Url); } } } // fallback to preview if latest is invalid - if (result.Version == null && result.PreviewVersion != null) + if (result.Main == null && result.Optional != null) { - result.Version = result.PreviewVersion; - result.Url = result.PreviewUrl; - result.PreviewVersion = null; - result.PreviewUrl = null; + result.Main = result.Optional; + result.Optional = null; } // special cases if (mod.ID == "Pathoschild.SMAPI") { result.Name = "SMAPI"; - result.Url = "https://smapi.io/"; - if (result.PreviewUrl != null) - result.PreviewUrl = "https://smapi.io/"; + if (result.Main != null) + result.Main.Url = "https://smapi.io/"; + if (result.Optional != null) + result.Optional.Url = "https://smapi.io/"; } // add result result.Errors = errors.ToArray(); + result.SetBackwardsCompatibility(apiVersion); mods[mod.ID] = result; } @@ -199,7 +201,8 @@ namespace StardewModdingAPI.Web.Controllers /// Get the mods for which the API should return data. /// The search model. - private IEnumerable GetSearchMods(ModSearchModel model) + /// The requested API version. + private IEnumerable GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion) { if (model == null) yield break; @@ -212,7 +215,7 @@ namespace StardewModdingAPI.Web.Controllers } // yield mod update keys if backwards compatible - if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17")) { foreach (string updateKey in model.ModKeys.Distinct()) yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); @@ -247,12 +250,11 @@ namespace StardewModdingAPI.Web.Controllers }); } - /// Get whether the API should return data in a backwards compatible way. - /// The last version for which data should be backwards compatible. - private bool ShouldBeBackwardsCompatible(string maxVersion) + /// Get the requested API version. + private ISemanticVersion GetApiVersion() { string actualVersion = (string)this.RouteData.Values["version"]; - return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + return new SemanticVersion(actualVersion); } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 4c879c8d..09c59a86 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs index ccb0699c..18252298 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { /// Generic metadata about a mod. - public class ModInfoModel + internal class ModInfoModel { /********* ** Accessors diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 150ed34a..a1180474 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -596,8 +596,8 @@ namespace StardewModdingAPI try { ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; - ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; - ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; + ISemanticVersion latestStable = response.Main?.Version; + ISemanticVersion latestBeta = response.Optional?.Version; if (latestStable == null && response.Errors.Any()) { @@ -673,18 +673,14 @@ namespace StardewModdingAPI // parse versions ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - ISemanticVersion latestVersion = result.Version != null - ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) - : null; - ISemanticVersion optionalVersion = result.PreviewVersion != null - ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) - : null; + ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; + ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; // show update alerts if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) - updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) - updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); + updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); } // show update errors diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index e4ab168e..581a524c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -12,19 +14,50 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod name. public string Name { get; set; } + /// The main version. + public ModEntryVersionModel Main { get; set; } + + /// The latest optional version, if newer than . + public ModEntryVersionModel Optional { get; set; } + + /// The errors that occurred while fetching update data. + public string[] Errors { get; set; } = new string[0]; + + /**** + ** Backwards-compatible fields + ****/ /// The mod's latest version number. - public string Version { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Main))] + internal string Version { get; private set; } /// The mod's web URL. - public string Url { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Main))] + internal string Url { get; private set; } /// The mod's latest optional release, if newer than . - public string PreviewVersion { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Optional))] + internal string PreviewVersion { get; private set; } /// The web URL to the mod's latest optional release, if newer than . - public string PreviewUrl { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Optional))] + internal string PreviewUrl { get; private set; } - /// The errors that occurred while fetching update data. - public string[] Errors { get; set; } = new string[0]; + + /********* + ** Public methods + *********/ + /// Set backwards-compatible fields. + /// The requested API version. + public void SetBackwardsCompatibility(ISemanticVersion version) + { + if (version.IsOlderThan("2.6-beta.19")) + { + this.Version = this.Main?.Version?.ToString(); + this.Url = this.Main?.Url; + + this.PreviewVersion = this.Optional?.Version?.ToString(); + this.PreviewUrl = this.Optional?.Url; + } + } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs new file mode 100644 index 00000000..dadb8c10 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs @@ -0,0 +1,31 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a version. + public class ModEntryVersionModel + { + /********* + ** Accessors + *********/ + /// The version number. + public ISemanticVersion Version { get; set; } + + /// The mod page URL. + public string Url { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModEntryVersionModel() { } + + /// Construct an instance. + /// The version number. + /// The mod page URL. + public ModEntryVersionModel(ISemanticVersion version, string url) + { + this.Version = version; + this.Url = url; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 3601fc53..237f2c66 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -40,12 +40,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// Get a semantic remote version for update checks. /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version) { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + if (version == null) + return null; + + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString()); return rawVersion != null ? new SemanticVersion(rawVersion) - : null; + : version; } } } -- cgit From c9fedebaf3231a2d5a00a95ff1ffd3ac5dac4ae2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 22:30:34 -0400 Subject: add support for unofficial version in update checks (#532) --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 47 +++++++++++++++++++++- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 3 ++ src/SMAPI.Web/appsettings.json | 4 +- src/SMAPI/Program.cs | 3 ++ .../Framework/Clients/WebApi/ModEntryModel.cs | 3 ++ 6 files changed, 58 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index a8f6d851..14cf78a2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * Improved update checks: * added beta update channel; * added support for optional files on Nexus; + * added support for unofficial updates from the wiki (only if the installed version is incompatible); * added console warning for mods which don't have update checks configured; * fixed mod update checks failing if a mod only has prerelease versions on GitHub; * fixed Nexus mod update alerts not showing HTTPS links. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c4f1023b..b9af17dc 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -45,6 +46,9 @@ namespace StardewModdingAPI.Web.Controllers /// The internal mod metadata list. private readonly ModDatabase ModDatabase; + /// The web URL for the wiki compatibility list. + private readonly string WikiCompatibilityPageUrl; + /********* ** Public methods @@ -60,6 +64,7 @@ namespace StardewModdingAPI.Web.Controllers { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; + this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl; this.Cache = cache; this.SuccessCacheMinutes = config.SuccessCacheMinutes; @@ -84,6 +89,9 @@ namespace StardewModdingAPI.Web.Controllers ISemanticVersion apiVersion = this.GetApiVersion(); ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray(); + // fetch wiki data + WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); + // perform checks IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) @@ -123,7 +131,7 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Main == null || result.Main.Version.IsOlderThan(version)) + if (this.IsNewer(version, result.Main?.Version)) { result.Name = data.Name; result.Main = new ModEntryVersionModel(version, data.Url); @@ -139,7 +147,7 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Optional == null || result.Optional.Version.IsOlderThan(version)) + if (this.IsNewer(version, result.Optional?.Version)) { result.Name = result.Name ?? data.Name; result.Optional = new ModEntryVersionModel(version, data.Url); @@ -147,6 +155,13 @@ namespace StardewModdingAPI.Web.Controllers } } + // get unofficial version + { + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); + } + // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) { @@ -199,6 +214,14 @@ namespace StardewModdingAPI.Web.Controllers return true; } + /// Get whether a version is newer than an version. + /// The current version. + /// The other version. + private bool IsNewer(ISemanticVersion current, ISemanticVersion other) + { + return current != null && (other == null || other.IsOlderThan(current)); + } + /// Get the mods for which the API should return data. /// The search model. /// The requested API version. @@ -222,6 +245,26 @@ namespace StardewModdingAPI.Web.Controllers } } + /// Get mod data from the wiki compatibility list. + private async Task GetWikiDataAsync() + { + ModToolkit toolkit = new ModToolkit(); + return await this.Cache.GetOrCreateAsync($"_wiki", async entry => + { + try + { + WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync(); + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes); + return entries; + } + catch + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes); + return new WikiCompatibilityEntry[0]; + } + }); + } + /// Get the mod info for an update key. /// The namespaced update key. private async Task GetInfoForUpdateKeyAsync(string updateKey) diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index fc3b7dc2..ce4f3cb5 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -24,5 +24,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The repository key for Nexus Mods. public string NexusKey { get; set; } + + /// The web URL for the wiki compatibility list. + public string WikiCompatibilityPageUrl { get; set; } } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 2f87dbe5..837ba536 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -46,6 +46,8 @@ "ChucklefishKey": "Chucklefish", "GitHubKey": "GitHub", - "NexusKey": "Nexus" + "NexusKey": "Nexus", + + "WikiCompatibilityPageUrl": "https://smapi.io/compat" } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a1180474..d899e512 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -675,12 +675,15 @@ namespace StardewModdingAPI ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; + ISemanticVersion unofficialVersion = result.Unofficial?.Version; // show update alerts if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); + else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) + updates.Add(Tuple.Create(mod, unofficialVersion, result.Unofficial?.Url)); } // show update errors diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 581a524c..adfdfef9 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest optional version, if newer than . public ModEntryVersionModel Optional { get; set; } + /// The latest unofficial version, if newer than and . + public ModEntryVersionModel Unofficial { get; set; } + /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; -- cgit From a0888e0ad1bf0ed38982d2aadf78c31d046b061b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:01:57 -0400 Subject: add optional extended metadata to mods API (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 19 ++---- .../Framework/Clients/WebApi/ModEntryModel.cs | 6 +- .../Clients/WebApi/ModExtendedMetadataModel.cs | 77 ++++++++++++++++++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 3 + .../Framework/ModData/ModDataRecord.cs | 10 +++ 5 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index b9af17dc..e5ae3fc7 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -132,10 +132,7 @@ namespace StardewModdingAPI.Web.Controllers } if (this.IsNewer(version, result.Main?.Version)) - { - result.Name = data.Name; result.Main = new ModEntryVersionModel(version, data.Url); - } } // handle optional version @@ -148,19 +145,14 @@ namespace StardewModdingAPI.Web.Controllers } if (this.IsNewer(version, result.Optional?.Version)) - { - result.Name = result.Name ?? data.Name; result.Optional = new ModEntryVersionModel(version, data.Url); - } } } // get unofficial version - { - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); - if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - } + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) @@ -172,13 +164,16 @@ namespace StardewModdingAPI.Web.Controllers // special cases if (mod.ID == "Pathoschild.SMAPI") { - result.Name = "SMAPI"; if (result.Main != null) result.Main.Url = "https://smapi.io/"; if (result.Optional != null) result.Optional.Url = "https://smapi.io/"; } + // add extended metadata + if (model.IncludeExtendedMetadata && (wikiEntry != null || record != null)) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // add result result.Errors = errors.ToArray(); result.SetBackwardsCompatibility(apiVersion); diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index adfdfef9..b311bd3b 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -11,9 +11,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod's unique ID (if known). public string ID { get; set; } - /// The mod name. - public string Name { get; set; } - /// The main version. public ModEntryVersionModel Main { get; set; } @@ -23,6 +20,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest unofficial version, if newer than and . public ModEntryVersionModel Unofficial { get; set; } + /// Optional extended data which isn't needed for update checks. + public ModExtendedMetadataModel Metadata { get; set; } + /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs new file mode 100644 index 00000000..a716114b --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -0,0 +1,77 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Extended metadata about a mod. + public class ModExtendedMetadataModel + { + /********* + ** Accessors + *********/ + /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + public string[] ID { get; set; } = new string[0]; + + /// The mod's display name. + public string Name { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + /// The compatibility status. + [JsonConverter(typeof(StringEnumConverter))] + public WikiCompatibilityStatus? CompatibilityStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + public string CompatibilitySummary { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModExtendedMetadataModel() { } + + /// Construct an instance. + /// The mod metadata from the wiki (if available). + /// The mod metadata from SMAPI's internal DB (if available). + public ModExtendedMetadataModel(WikiCompatibilityEntry wiki, ModDataRecord db) + { + // wiki data + if (wiki != null) + { + this.ID = wiki.ID; + this.Name = wiki.Name; + this.NexusID = wiki.NexusID; + this.ChucklefishID = wiki.ChucklefishID; + this.GitHubRepo = wiki.GitHubRepo; + this.CustomSourceUrl = wiki.CustomSourceUrl; + this.CustomUrl = wiki.CustomUrl; + this.CompatibilityStatus = wiki.Status; + this.CompatibilitySummary = wiki.Summary; + } + + // internal DB data + if (db != null) + { + this.ID = this.ID.Union(db.FormerIDs).ToArray(); + this.Name = this.Name ?? db.DisplayName; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index ffca32ca..754cf02c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mods for which to find data. public ModSearchEntryModel[] Mods { get; set; } + /// Whether to include extended metadata for each mod. + public bool IncludeExtendedMetadata { get; set; } + /********* ** Public methods diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 21c9cfca..82ac8837 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -86,6 +86,16 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData : version; } + /// Get the possible mod IDs. + public IEnumerable GetIDs() + { + return this.FormerIDs + .Concat(new[] { this.ID }) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .Distinct(); + } + /// Get a parsed representation of the which match a given manifest. /// The manifest to match. public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) -- cgit From 60b38666e29684650108031f08ca30bfe483ceab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:27:31 -0400 Subject: simplify mod API response structure (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 154 +++++++++++---------- .../Framework/Clients/WebApi/WebApiClient.cs | 5 +- 2 files changed, 87 insertions(+), 72 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index e5ae3fc7..b500e19d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -83,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model) + public async Task PostAsync([FromBody] ModSearchModel model) { // parse request data ISemanticVersion apiVersion = this.GetApiVersion(); @@ -92,101 +92,115 @@ namespace StardewModdingAPI.Web.Controllers // fetch wiki data WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); - // perform checks + // fetch data IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) { if (string.IsNullOrWhiteSpace(mod.ID)) continue; - // resolve update keys - var updateKeys = new HashSet(mod.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); - ModDataRecord record = this.ModDatabase.Get(mod.ID); - if (record?.Fields != null) + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); + result.SetBackwardsCompatibility(apiVersion); + mods[mod.ID] = result; + } + + // return in expected structure + return apiVersion.IsNewerThan("2.6-beta.18") + ? mods.Values + : (object)mods; + } + + + /********* + ** Private methods + *********/ + /// Get the metadata for a mod. + /// The mod data to match. + /// The wiki data. + /// Whether to include extended metadata for each mod. + /// Returns the mod data if found, else null. + private async Task GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata) + { + // resolve update keys + var updateKeys = new HashSet(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + ModDataRecord record = this.ModDatabase.Get(search.ID); + if (record?.Fields != null) + { + string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) + updateKeys.Add(defaultUpdateKey); + } + + // get latest versions + ModEntryModel result = new ModEntryModel { ID = search.ID }; + IList errors = new List(); + foreach (string updateKey in updateKeys) + { + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) { - string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; - if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) - updateKeys.Add(defaultUpdateKey); + errors.Add(data.Error); + continue; } - // get latest versions - ModEntryModel result = new ModEntryModel { ID = mod.ID }; - IList errors = new List(); - foreach (string updateKey in updateKeys) + // handle main version + if (data.Version != null) { - // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); - if (data.Error != null) + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) { - errors.Add(data.Error); + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } - // handle main version - if (data.Version != null) - { - if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); - continue; - } - - if (this.IsNewer(version, result.Main?.Version)) - result.Main = new ModEntryVersionModel(version, data.Url); - } + if (this.IsNewer(version, result.Main?.Version)) + result.Main = new ModEntryVersionModel(version, data.Url); + } - // handle optional version - if (data.PreviewVersion != null) + // handle optional version + if (data.PreviewVersion != null) + { + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) { - if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); - continue; - } - - if (this.IsNewer(version, result.Optional?.Version)) - result.Optional = new ModEntryVersionModel(version, data.Url); + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; } - } - // get unofficial version - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); - if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - - // fallback to preview if latest is invalid - if (result.Main == null && result.Optional != null) - { - result.Main = result.Optional; - result.Optional = null; + if (this.IsNewer(version, result.Optional?.Version)) + result.Optional = new ModEntryVersionModel(version, data.Url); } + } - // special cases - if (mod.ID == "Pathoschild.SMAPI") - { - if (result.Main != null) - result.Main.Url = "https://smapi.io/"; - if (result.Optional != null) - result.Optional.Url = "https://smapi.io/"; - } + // get unofficial version + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - // add extended metadata - if (model.IncludeExtendedMetadata && (wikiEntry != null || record != null)) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // fallback to preview if latest is invalid + if (result.Main == null && result.Optional != null) + { + result.Main = result.Optional; + result.Optional = null; + } - // add result - result.Errors = errors.ToArray(); - result.SetBackwardsCompatibility(apiVersion); - mods[mod.ID] = result; + // special cases + if (result.ID == "Pathoschild.SMAPI") + { + if (result.Main != null) + result.Main.Url = "https://smapi.io/"; + if (result.Optional != null) + result.Optional.Url = "https://smapi.io/"; } - return mods; - } + // add extended metadata + if (includeExtendedMetadata && (wikiEntry != null || record != null)) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // add result + result.Errors = errors.ToArray(); + return result; + } - /********* - ** Private methods - *********/ /// Parse a namespaced mod ID. /// The raw mod ID to parse. /// The parsed vendor key. diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 892dfeba..3e412fc3 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using Newtonsoft.Json; @@ -34,10 +35,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod keys for which to fetch the latest version. public IDictionary GetModInfo(params ModSearchEntryModel[] mods) { - return this.Post>( + return this.Post( $"v{this.Version}/mods", new ModSearchModel(mods) - ); + ).ToDictionary(p => p.ID); } -- cgit From 68287c983c42b1aff66a695fa663d651482f240e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:28:25 -0400 Subject: add remote version mapping for unofficial Almighty Farming Tool update --- src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json index 343257f1..d9b2954c 100644 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -100,7 +100,10 @@ "Almighty Farming Tool": { "ID": "439", - "MapRemoteVersions": { "1.21": "1.2.1" }, + "MapRemoteVersions": { + "1.21": "1.2.1", + "1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion" + }, "Default | UpdateKey": "Nexus:439" }, -- cgit From c0370c54113bc95919affcbfbba8720a42b97a30 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:50:06 -0400 Subject: add includeExtendedMetadata option to toolkit client (#532) --- src/SMAPI/Program.cs | 2 +- .../Framework/Clients/WebApi/ModSeachModel.cs | 4 +++- .../Framework/Clients/WebApi/WebApiClient.cs | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d899e512..a88db105 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -595,7 +595,7 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; ISemanticVersion latestStable = response.Main?.Version; ISemanticVersion latestBeta = response.Optional?.Version; diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index 754cf02c..df0d8457 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -31,9 +31,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The mods to search. - public ModSearchModel(ModSearchEntryModel[] mods) + /// Whether to include extended metadata for each mod. + public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) { this.Mods = mods.ToArray(); + this.IncludeExtendedMetadata = includeExtendedMetadata; } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 3e412fc3..5bbe473e 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -33,11 +33,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Get metadata about a set of mods from the web API. /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params ModSearchEntryModel[] mods) + /// Whether to include extended metadata for each mod. + public IDictionary GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) { return this.Post( $"v{this.Version}/mods", - new ModSearchModel(mods) + new ModSearchModel(mods, includeExtendedMetadata) ).ToDictionary(p => p.ID); } -- cgit From 86428a31c2c275b32f08f149157f2fad78c8e488 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:54:49 -0400 Subject: fix web API client not using correct JSON settings (#532) --- .../Framework/Clients/WebApi/WebApiClient.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 5bbe473e..0ecd9664 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -18,6 +19,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The API version number. private readonly ISemanticVersion Version; + /// The JSON serializer settings to use. + private readonly JsonSerializerSettings JsonSettings = new JsonHelper().JsonSettings; + /********* ** Public methods @@ -62,7 +66,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi client.Headers["Content-Type"] = "application/json"; client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response); + return JsonConvert.DeserializeObject(response, this.JsonSettings); } } } -- cgit From d67690ea3ea76512af194168af02a29120765247 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 00:00:29 -0400 Subject: fix new DLL not referenced by build config package on Linux/Mac (#532) --- src/SMAPI.ModBuildConfig/build/smapi.targets | 5 +++++ src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 9946e1a6..f5691fec 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -131,6 +131,11 @@ false true + + $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + false + true + $(GamePath)\xTile.dll false diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 789868a0..68746648 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0-beta-20180625 + 2.1.0-beta-20180629 Build package for SMAPI mods Pathoschild Pathoschild -- cgit From 79ad322a8e31e977e455817469427f9c3cf218fd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 13:40:48 -0400 Subject: tweak world-ready events to handle edge cases In particular: - world was never considered ready if the player's name was blank; - AfterReturnToTitle didn't trigger after being disconnected in multiplayer (#545). --- docs/release-notes.md | 1 + src/SMAPI/Context.cs | 2 +- src/SMAPI/Framework/SGame.cs | 31 ++++++++++++++----------------- 3 files changed, 16 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 14cf78a2..2b449ac7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,6 +26,7 @@ * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. + * Fixed many mods not working if the player name is blank. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * Updated compatibility list. diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index 79067b2d..74def086 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI ** Internal ****/ /// Whether a player save has been loaded. - internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.Name); + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !(Game1.activeClickableMenu is TitleMenu); /// Whether the game is currently writing to the save file. internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 57afec06..def0943c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Framework /// The number of ticks until SMAPI should notify mods that the game has loaded. /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. - private int AfterLoadTimer = 5; + private readonly Countdown AfterLoadTimer = new Countdown(5); /// Whether the after-load events were raised for this session. private bool RaisedAfterLoadEvent; @@ -313,13 +313,14 @@ namespace StardewModdingAPI.Framework /********* ** Update context *********/ - if (Context.IsWorldReady && !Context.IsSaveLoaded) + bool wasWorldReady = Context.IsWorldReady; + if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle) this.MarkWorldNotReady(); - else if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0 && Game1.currentLocation != null) + else if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) { if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) - this.AfterLoadTimer--; - Context.IsWorldReady = this.AfterLoadTimer <= 0; + this.AfterLoadTimer.Decrement(); + Context.IsWorldReady = this.AfterLoadTimer.Current == 0; } /********* @@ -342,9 +343,14 @@ namespace StardewModdingAPI.Framework } /********* - ** After load events + ** Load / return-to-title events *********/ - if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) + if (wasWorldReady && !Context.IsWorldReady) + { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + this.Events.Save_AfterReturnToTitle.Raise(); + } + else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady) { // print context string context = $"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}."; @@ -363,15 +369,6 @@ namespace StardewModdingAPI.Framework this.Events.Time_AfterDayStarted.Raise(); } - /********* - ** Exit to title events - *********/ - if (Game1.exitToTitle) - { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - this.Events.Save_AfterReturnToTitle.Raise(); - } - /********* ** Window events *********/ @@ -1342,7 +1339,7 @@ namespace StardewModdingAPI.Framework private void MarkWorldNotReady() { Context.IsWorldReady = false; - this.AfterLoadTimer = 5; + this.AfterLoadTimer.Reset(); this.RaisedAfterLoadEvent = false; } -- cgit From 4b646e4f927f4c44004b3b233b6be055a645b563 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 14:54:15 -0400 Subject: generalise NuGet package's non-mod project feature (#555) --- docs/mod-build-config.md | 16 +++++++------ src/SMAPI.ModBuildConfig/build/smapi.targets | 34 ++++++++++++---------------- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 3 files changed, 25 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 32762580..0c1cc10a 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -131,18 +131,20 @@ If you don't want to include a file in the mod folder or release zip: This is a comma-delimited list of regular expression patterns. If any pattern matches a file's relative path in your mod folder, that file won't be included. -### Unit test projects +### Non-mod projects **(upcoming in 2.1)** -You can use the package in unit test projects too. Its optional unit test mode... +You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). You'll need to +disable deploying the mod and creating a release zip: -1. disables deploying the project as a mod; -2. disables creating a release zip; -2. and copies the referenced DLLs into the build output for unit test frameworks. +```xml +False +False +``` -To enable it, add this above the first `` in your `.csproj`: +If this is for unit tests, you may need to copy the referenced DLLs into your build output too: ```xml -True +True ``` ## Code warnings diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index f5691fec..8ca178cc 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -19,14 +19,10 @@ $(MSBuildProjectName) - False $(TargetDir) True True - - - False - False + False @@ -63,45 +59,45 @@ false - true + true false - true + true false - true + true false - true + true $(GamePath)\Netcode.dll False - true + true $(GamePath)\Stardew Valley.exe false - true + true $(GamePath)\StardewModdingAPI.exe false - true + true $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll false - true + true $(GamePath)\xTile.dll false False - true + true @@ -119,27 +115,27 @@ $(GamePath)\MonoGame.Framework.dll false False - true + true $(GamePath)\StardewValley.exe false - true + true $(GamePath)\StardewModdingAPI.exe false - true + true $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll false - true + true $(GamePath)\xTile.dll false - true + true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 68746648..cb6e41e1 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -14,7 +14,7 @@ 2.1: - Added support for Stardew Valley 1.3. - - Added support for unit test projects. + - Added support for non-mod projects. - Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3. - Added option to ignore files by regex pattern. - Added reference to new SMAPI DLL. -- cgit From 74c747e20cf184d8ec4cced0bc0d1bbc3c78b3fd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 14:54:29 -0400 Subject: fix NuGet package validating required files when they're not needed (#555) --- src/SMAPI.ModBuildConfig/DeployModTask.cs | 2 +- .../Framework/ModFileManager.cs | 23 +++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 73971279..96d95e06 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.ModBuildConfig Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray(); // get mod info - ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns); + ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip); // deploy mod files if (this.EnableModDeploy) diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 524aeaf7..f4738d71 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -28,8 +28,9 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /// The folder containing the project files. /// The folder containing the build output. /// Custom regex patterns matching files to ignore when deploying or zipping the mod. + /// Whether to validate that required mod files like the manifest are present. /// The mod package isn't valid. - public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns) + public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns, bool validateRequiredModFiles) { this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -82,14 +83,18 @@ namespace StardewModdingAPI.ModBuildConfig.Framework this.Files[relativePath] = file; } - // check for missing manifest - if (!this.Files.ContainsKey(this.ManifestFileName)) - throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); - - // check for missing DLL - // ReSharper disable once SimplifyLinqExpression - if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) - throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); + // check for required files + if (validateRequiredModFiles) + { + // manifest + if (!this.Files.ContainsKey(this.ManifestFileName)) + throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); + + // DLL + // ReSharper disable once SimplifyLinqExpression + if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) + throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); + } } /// Get the files in the mod package. -- cgit From 053a8fd5811e22a83b21f7d5b89ce2894cbe8183 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 14:54:41 -0400 Subject: bump NuGet package version for beta release --- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index cb6e41e1..bb142c33 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0-beta-20180629 + 2.1.0-beta-20180630 Build package for SMAPI mods Pathoschild Pathoschild -- cgit From 96a8401c03bc38b682a5a793fb4d15766a2663b2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 15:38:29 -0400 Subject: fix types getting rewritten unnecessarily if the source & target types have the same full name (#556) --- src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index cf840dcc..62e15731 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -92,7 +92,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Whether the mod was compiled on a different platform. public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) + if (!this.IsMatch(instruction)) return InstructionHandleResult.None; // field reference -- cgit From 8b9d1baaea415dfb3d845e990898c29c024c5c18 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 16:55:59 -0400 Subject: fix Context.IsPlayerFree being false during festivals (#550) --- docs/release-notes.md | 3 ++- src/SMAPI/Context.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2b449ac7..ae7f766d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -48,11 +48,12 @@ * Added absolute pixels to `ICursorPosition`. * Added support for reading/writing `ISemanticVersion` to JSON. * Update checks now use the update key order when deciding which to link to. - * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. + * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. + * Fixed `Context.IsPlayerFree` being false during festivals. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * Fixed some common non-mod build output being included in release zip. * Fixed mods able to intercept other mods' assets via the internal asset keys. diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index 74def086..3905699e 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI public static bool IsWorldReady { get; internal set; } /// Whether is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc). - public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && !Game1.eventUp; + public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && (!Game1.eventUp || Game1.isFestival()); /// Whether is true and the player is free to move (e.g. not using a tool). public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; -- cgit From 599f5851928c82dba118e1d302da731a4ed4f91d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 17:09:29 -0400 Subject: remove player_setlevel and player_setspeed commands (#415) --- docs/release-notes.md | 1 + .../Framework/Commands/Player/SetLevelCommand.cs | 90 ---------------------- .../Framework/Commands/Player/SetSpeedCommand.cs | 30 -------- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 2 - 4 files changed, 1 insertion(+), 122 deletions(-) delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ae7f766d..adf161b4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -28,6 +28,7 @@ * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. * Fixed many mods not working if the player name is blank. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. + * Removed the `player_setspeed` and `player_setlevel` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those. * Updated compatibility list. * For modders: diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs deleted file mode 100644 index 97a36066..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Generic; -using StardewValley; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player -{ - /// A command which edits the player's current level for a skill. - internal class SetLevelCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// The experience points needed to reach each level. - /// Derived from . - private readonly IDictionary LevelExp = new Dictionary - { - [0] = 0, - [1] = 100, - [2] = 380, - [3] = 770, - [4] = 1300, - [5] = 2150, - [6] = 3300, - [7] = 4800, - [8] = 6900, - [9] = 10000, - [10] = 15000 - }; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetLevelCommand() - : base("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel \n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).") { } - - /// 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) - { - // validate - if (!args.TryGet(0, "skill", out string skill, oneOf: new[] { "luck", "mining", "combat", "farming", "fishing", "foraging" })) - return; - if (!args.TryGetInt(1, "level", out int level, min: 0, max: 10)) - return; - - // handle - switch (skill) - { - case "luck": - Game1.player.LuckLevel = level; - Game1.player.experiencePoints[Farmer.luckSkill] = this.LevelExp[level]; - monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info); - break; - - case "mining": - Game1.player.MiningLevel = level; - Game1.player.experiencePoints[Farmer.miningSkill] = this.LevelExp[level]; - monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info); - break; - - case "combat": - Game1.player.CombatLevel = level; - Game1.player.experiencePoints[Farmer.combatSkill] = this.LevelExp[level]; - monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info); - break; - - case "farming": - Game1.player.FarmingLevel = level; - Game1.player.experiencePoints[Farmer.farmingSkill] = this.LevelExp[level]; - monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info); - break; - - case "fishing": - Game1.player.FishingLevel = level; - Game1.player.experiencePoints[Farmer.fishingSkill] = this.LevelExp[level]; - monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info); - break; - - case "foraging": - Game1.player.ForagingLevel = level; - Game1.player.experiencePoints[Farmer.foragingSkill] = this.LevelExp[level]; - monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info); - break; - } - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs deleted file mode 100644 index e9693540..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using StardewValley; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player -{ - /// A command which edits the player's current added speed. - internal class SetSpeedCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetSpeedCommand() - : base("player_setspeed", "Sets the player's added speed to the specified value.\n\nUsage: player_setspeed \n- value: an integer amount (0 is normal).") { } - - /// 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) - { - // parse arguments - if (!args.TryGetInt(0, "added speed", out int amount, min: 0)) - return; - - // handle - Game1.player.addedSpeed = amount; - monitor.Log($"OK, your added speed is now {Game1.player.addedSpeed}.", LogLevel.Info); - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index 357f34d7..50b7b87f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -56,8 +56,6 @@ - - -- cgit From 34b0fd2870f0057b146dbac812cb7d673f2b11a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 18:00:32 -0400 Subject: detect broken assembly references not covered by a dependency, and flag as incompatible (#356) --- docs/release-notes.md | 3 ++- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 27 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index adf161b4..80f6e604 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -27,8 +27,9 @@ * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. * Fixed many mods not working if the player name is blank. + * Fixed repeated errors in some cases when a mod references a missing assembly. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. - * Removed the `player_setspeed` and `player_setlevel` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those. + * Removed the `player_setlevel` and `player_setspeed` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those. * Updated compatibility list. * For modders: diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index ba4c3f5c..8f5a8316 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -99,6 +99,19 @@ namespace StardewModdingAPI.Framework.ModLoading // rewrite assembly bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + // detect broken assembly reference + foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) + { + if (!this.IsAssemblyLoaded(reference)) + { + this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'."); + if (!assumeCompatible) + throw new IncompatibleInstructionException($"assembly reference to {reference.FullName}", $"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}."); + mod.SetWarning(ModWarning.BrokenCodeLoaded); + break; + } + } + // load assembly if (changed) { @@ -126,6 +139,20 @@ namespace StardewModdingAPI.Framework.ModLoading return lastAssembly; } + /// Get whether an assembly is loaded. + /// The assembly name reference. + public bool IsAssemblyLoaded(AssemblyNameReference reference) + { + try + { + return this.AssemblyDefinitionResolver.Resolve(reference) != null; + } + catch (AssemblyResolutionException) + { + return false; + } + } + /// Resolve an assembly by its name. /// The assembly name. /// -- cgit From c12777ad53583997dd9e1ee074e8376da827fc84 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 21:00:45 -0400 Subject: move basic mod scanning into the toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 8 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 40 ++------ src/SMAPI/Program.cs | 16 ++-- .../Framework/ModScanning/ModFolder.cs | 60 ++++++++++++ .../Framework/ModScanning/ModScanner.cs | 103 +++++++++++++++++++++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 16 ++++ 6 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs (limited to 'src') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 9e91b993..a38621f8 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -7,8 +7,8 @@ using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Core @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(rootFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); @@ -46,7 +46,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(modFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -85,7 +85,7 @@ namespace StardewModdingAPI.Tests.Core File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 174820a1..9ac95fd4 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; @@ -17,38 +18,15 @@ namespace StardewModdingAPI.Framework.ModLoading ** Public methods *********/ /// Get manifest metadata for each folder in the given root path. + /// The mod toolkit. /// The root path to search for mods. - /// The JSON helper with which to read manifests. /// Handles access to SMAPI's internal mod metadata list. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) + public IEnumerable ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase) { - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) + foreach (ModFolder folder in toolkit.GetModFolders(rootPath)) { - // read file - Manifest manifest = null; - string error = null; - { - string path = Path.Combine(modDir.FullName, "manifest.json"); - try - { - manifest = jsonHelper.ReadJsonFile(path); - if (manifest == null) - { - error = File.Exists(path) - ? "its manifest is invalid." - : "it doesn't have a manifest."; - } - } - catch (SParseException ex) - { - error = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; - } - } + Manifest manifest = folder.Manifest; // parse internal data record (if any) ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); @@ -58,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); + displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName); // apply defaults if (manifest != null && dataRecord != null) @@ -68,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading } // build metadata - ModMetadataStatus status = error == null + ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); + yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a88db105..c9266c69 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -104,8 +104,8 @@ namespace StardewModdingAPI new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper = new JsonHelper(); + /// The mod toolkit used for generic mod interactions. + private readonly ModToolkit Toolkit = new ModToolkit(); /********* @@ -205,7 +205,7 @@ namespace StardewModdingAPI new RectangleConverter() }; foreach (JsonConverter converter in converters) - this.JsonHelper.JsonSettings.Converters.Add(converter); + this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); // add error handlers #if SMAPI_FOR_WINDOWS @@ -423,14 +423,14 @@ namespace StardewModdingAPI ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(toolkit, Constants.ModPath, modDatabase).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods - this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase); + this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // write metadata file if (this.Settings.DumpMetadata) @@ -443,7 +443,7 @@ namespace StardewModdingAPI ModFolderPath = Constants.ModPath, Mods = mods }; - this.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export); + this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export); } // check for updates @@ -875,7 +875,7 @@ namespace StardewModdingAPI { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); @@ -1117,7 +1117,7 @@ namespace StardewModdingAPI /// The mods for which to reload translations. private void ReloadTranslations(IEnumerable mods) { - JsonHelper jsonHelper = this.JsonHelper; + JsonHelper jsonHelper = this.Toolkit.JsonHelper; foreach (IModMetadata metadata in mods) { if (metadata.IsContentPack) diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs new file mode 100644 index 00000000..9b6853b4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// The info about a mod read from its folder. + public class ModFolder + { + /********* + ** Accessors + *********/ + /// The Mods subfolder containing this mod. + public DirectoryInfo SearchDirectory { get; } + + /// The folder containing manifest.json. + public DirectoryInfo ActualDirectory { get; } + + /// The mod manifest. + public Manifest Manifest { get; } + + /// The error which occurred parsing the manifest, if any. + public string ManifestParseError { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance when a mod wasn't found in a folder. + /// The directory that was searched. + public ModFolder(DirectoryInfo searchDirectory) + { + this.SearchDirectory = searchDirectory; + } + + /// Construct an instance. + /// The Mods subfolder containing this mod. + /// The folder containing manifest.json. + /// The mod manifest. + /// The error which occurred parsing the manifest, if any. + public ModFolder(DirectoryInfo searchDirectory, DirectoryInfo actualDirectory, Manifest manifest, string manifestParseError = null) + { + this.SearchDirectory = searchDirectory; + this.ActualDirectory = actualDirectory; + this.Manifest = manifest; + this.ManifestParseError = manifestParseError; + } + + /// Get the update keys for a mod. + /// The mod manifest. + public IEnumerable GetUpdateKeys(Manifest manifest) + { + return + (manifest.UpdateKeys ?? new string[0]) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .ToArray(); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs new file mode 100644 index 00000000..d3662c9c --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// Scans folders for mod data. + public class ModScanner + { + /********* + ** Properties + *********/ + /// The JSON helper with which to read manifests. + private readonly JsonHelper JsonHelper; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The JSON helper with which to read manifests. + public ModScanner(JsonHelper jsonHelper) + { + this.JsonHelper = jsonHelper; + } + + /// Extract information about all mods in the given folder. + /// The root folder containing mods. + public IEnumerable GetModFolders(string rootPath) + { + foreach (DirectoryInfo folder in new DirectoryInfo(rootPath).EnumerateDirectories()) + yield return this.ReadFolder(rootPath, folder); + } + + /// Extract information from a mod folder. + /// The root folder containing mods. + /// The folder to search for a mod. + public ModFolder ReadFolder(string rootPath, DirectoryInfo searchFolder) + { + // find manifest.json + FileInfo manifestFile = this.FindManifest(searchFolder); + if (manifestFile == null) + return new ModFolder(searchFolder); + + // read mod info + Manifest manifest = null; + string manifestError = null; + { + try + { + manifest = this.JsonHelper.ReadJsonFile(manifestFile.FullName); + if (manifest == null) + { + manifestError = File.Exists(manifestFile.FullName) + ? "its manifest is invalid." + : "it doesn't have a manifest."; + } + } + catch (SParseException ex) + { + manifestError = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + manifestError = $"parsing its manifest failed:\n{ex}"; + } + } + + return new ModFolder(searchFolder, manifestFile.Directory, manifest, manifestError); + } + + + /********* + ** Private methods + *********/ + /// Find the manifest for a mod folder. + /// The folder to search. + private FileInfo FindManifest(DirectoryInfo folder) + { + while (true) + { + // check for manifest in current folder + FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json")); + if (file.Exists) + return file; + + // check for single subfolder + FileSystemInfo[] entries = folder.EnumerateFileSystemInfos().Take(2).ToArray(); + if (entries.Length == 1 && entries[0] is DirectoryInfo subfolder) + { + folder = subfolder; + continue; + } + + // not found + return null; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 18fe1ff3..8c78b2f3 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Serialisation; namespace StardewModdingAPI.Toolkit { @@ -27,6 +29,13 @@ namespace StardewModdingAPI.Toolkit }; + /********* + ** Accessors + *********/ + /// Encapsulates SMAPI's JSON parsing. + public JsonHelper JsonHelper { get; } = new JsonHelper(); + + /********* ** Public methods *********/ @@ -53,6 +62,13 @@ namespace StardewModdingAPI.Toolkit return new ModDatabase(records, this.GetUpdateUrl); } + /// Extract information about all mods in the given folder. + /// The root folder containing mods. + public IEnumerable GetModFolders(string rootPath) + { + return new ModScanner(this.JsonHelper).GetModFolders(rootPath); + } + /// Get an update URL for an update key (if valid). /// The update key. public string GetUpdateUrl(string updateKey) -- cgit From 34c43f9f66b33c402947be5e84544e09cb048290 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 12:23:03 -0400 Subject: add toolkit method for API data (#532) --- .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index a716114b..21376b36 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -73,5 +74,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Name = this.Name ?? db.DisplayName; } } + + /// Get update keys based on the metadata. + public IEnumerable GetUpdateKeys() + { + if (this.NexusID.HasValue) + yield return $"Nexus:{this.NexusID}"; + if (this.ChucklefishID.HasValue) + yield return $"Chucklefish:{this.ChucklefishID}"; + if (this.GitHubRepo != null) + yield return $"GitHub:{this.GitHubRepo}"; + } } } -- cgit From e548a4ea9bf24c28ebc024e12af73cd14b76f05f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 13:02:08 -0400 Subject: fix assembly definition resolver not disposing loaded definitions --- src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index 33cd6ebd..91c9e192 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading ** Properties *********/ /// The known assemblies. - private readonly IDictionary Loaded = new Dictionary(); + private readonly IDictionary Lookup = new Dictionary(); /********* @@ -22,8 +22,9 @@ namespace StardewModdingAPI.Framework.ModLoading { foreach (AssemblyDefinition assembly in assemblies) { - this.Loaded[assembly.Name.Name] = assembly; - this.Loaded[assembly.Name.FullName] = assembly; + this.RegisterAssembly(assembly); + this.Lookup[assembly.Name.Name] = assembly; + this.Lookup[assembly.Name.FullName] = assembly; } } @@ -44,8 +45,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly's short or full name. private AssemblyDefinition ResolveName(string name) { - return this.Loaded.ContainsKey(name) - ? this.Loaded[name] + return this.Lookup.TryGetValue(name, out AssemblyDefinition match) + ? match : null; } } -- cgit From 5357de4219c60fb44eb204e4f6c06e062a41d964 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 15:15:33 -0400 Subject: add game path to assembly definition resolution search paths for rare edge cases where it's not added automatically --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 80f6e604..b850016d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -28,6 +28,7 @@ * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop. * Fixed many mods not working if the player name is blank. * Fixed repeated errors in some cases when a mod references a missing assembly. + * Fixed `AssemblyResolutionException` errors in rare cases. * Renamed `install.exe` to `install on Windows.exe` to avoid confusion. * Removed the `player_setlevel` and `player_setspeed` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those. * Updated compatibility list. diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 8f5a8316..2f6130de 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -44,6 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor = monitor; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary(); -- cgit From eb8ba0576a4e7dc7f6c7963a268297834f501d16 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 17:33:41 -0400 Subject: fix incorrect trace message --- src/SMAPI/Framework/ContentCoordinator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 1336f3e9..d9b2109a 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -250,7 +250,8 @@ namespace StardewModdingAPI.Framework // report result if (removedAssetNames.Any()) this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + else + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); return removedAssetNames; } -- cgit From d8ee422405614a9b5d56ae0f42a003835fc57a8c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 17:37:19 -0400 Subject: add support for reloading NPC schedules through the content API --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 38 ++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ae078e20..bbf04556 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -48,6 +48,7 @@ * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. * Added absolute pixels to `ICursorPosition`. * Added support for reading/writing `ISemanticVersion` to JSON. + * Added support for reloading NPC schedules through the content API. * Improved update alerts to use update key order when choosing a display URL. * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 7ca0bd82..12abeb10 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -325,6 +325,10 @@ namespace StardewModdingAPI.Metadata if (this.IsInFolder(key, "Portraits")) return this.ReloadNpcPortraits(content, key); + // dynamic data + if (this.IsInFolder(key, "Characters\\schedules")) + return this.ReloadNpcSchedules(content, key); + return false; } @@ -333,7 +337,7 @@ namespace StardewModdingAPI.Metadata ** Private methods *********/ /**** - ** Reload methods + ** Reload texture methods ****/ /// Reload the sprites for matching pets or horses. /// The animal type. @@ -501,6 +505,38 @@ namespace StardewModdingAPI.Metadata return false; } + /**** + ** Reload data methods + ****/ + /// Reload the schedules for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any assets were reloaded. + private bool ReloadNpcSchedules(LocalizedContentManager content, string key) + { + // get NPCs + string name = Path.GetFileName(key); + NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); + if (!villagers.Any()) + return false; + + // update schedule + foreach (NPC villager in villagers) + { + // reload schedule + villager.Schedule = villager.getSchedule(Game1.dayOfMonth); + + // switch to new schedule if needed + int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); + if (lastScheduleTime != 0) + { + this.Reflection.GetField(villager, "scheduleTimeToTry").SetValue(this.Reflection.GetField(typeof(NPC), "NO_TRY").GetValue()); // use time that's passed in to checkSchedule + villager.checkSchedule(lastScheduleTime); + } + } + return true; + } + /**** ** Helpers ****/ -- cgit From aa13941dd7bceb8f619cd96f61d954c8aed55502 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 17:39:46 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index f240faab..1179b2de 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.18", + "Version": "2.6.0-beta.18.1", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 7314b62a..bf8ca5ff 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.18", + "Version": "2.6.0-beta.18.1", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index c4a3813e..ec6ce406 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.18"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.18.1"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); -- cgit From 82ca09ead7316486f7cd6fc2bc7879d94902bf90 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Jul 2018 20:41:21 -0400 Subject: fix 'missing assembly' errors raised for some .NET Framework types (#356) --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 2f6130de..37b1a378 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework.ModLoading // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) { - if (!this.IsAssemblyLoaded(reference)) + if (!reference.Name.StartsWith("System.") && !this.IsAssemblyLoaded(reference)) { this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'."); if (!assumeCompatible) -- cgit From 703acdc63f8543bab01f38ea68c28befb2911df8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Jul 2018 21:33:53 -0400 Subject: fix backwards-compatible API fields not being serialised (#532) --- .../Framework/Clients/WebApi/ModEntryModel.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index b311bd3b..f3f22b93 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -31,18 +32,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ****/ /// The mod's latest version number. [Obsolete("Use " + nameof(ModEntryModel.Main))] + [JsonProperty] internal string Version { get; private set; } /// The mod's web URL. [Obsolete("Use " + nameof(ModEntryModel.Main))] + [JsonProperty] internal string Url { get; private set; } /// The mod's latest optional release, if newer than . [Obsolete("Use " + nameof(ModEntryModel.Optional))] + [JsonProperty] internal string PreviewVersion { get; private set; } /// The web URL to the mod's latest optional release, if newer than . [Obsolete("Use " + nameof(ModEntryModel.Optional))] + [JsonProperty] internal string PreviewUrl { get; private set; } -- cgit From 7907a63ddc4285e9f2b8178be4b1d846ada03551 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Jul 2018 22:29:03 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 1179b2de..1f8c7b9f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.18.1", + "Version": "2.6.0-beta.19", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index bf8ca5ff..80990f8d 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.18.1", + "Version": "2.6.0-beta.19", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index ec6ce406..09b21b83 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.18.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); -- cgit From 34b1dcc1f75651c967257d37a0f29f24335c8110 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 3 Jul 2018 01:59:45 -0400 Subject: fix missing manifest not marking mod invalid (#532) --- src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 7 ------- src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 8 ++------ 2 files changed, 2 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 9b6853b4..4aaa3f83 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -27,13 +27,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /********* ** Public methods *********/ - /// Construct an instance when a mod wasn't found in a folder. - /// The directory that was searched. - public ModFolder(DirectoryInfo searchDirectory) - { - this.SearchDirectory = searchDirectory; - } - /// Construct an instance. /// The Mods subfolder containing this mod. /// The folder containing manifest.json. diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index d3662c9c..de8d0f02 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); if (manifestFile == null) - return new ModFolder(searchFolder); + return new ModFolder(searchFolder, null, null, "it doesn't have a manifest."); // read mod info Manifest manifest = null; @@ -53,11 +53,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { manifest = this.JsonHelper.ReadJsonFile(manifestFile.FullName); if (manifest == null) - { - manifestError = File.Exists(manifestFile.FullName) - ? "its manifest is invalid." - : "it doesn't have a manifest."; - } + manifestError = "its manifest is invalid."; } catch (SParseException ex) { -- cgit From ea264fb6a190321be39c8094ee4e2afff870304e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 3 Jul 2018 02:01:41 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 1f8c7b9f..dc5ea3f2 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.19", + "Version": "2.6.0-beta.19.1", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 80990f8d..eb2f0abc 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.19", + "Version": "2.6.0-beta.19.1", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 09b21b83..b9410153 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19.1"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); -- cgit From 186ab4cca7c1b3ae67a68bf6a3a3b1e7cdcb7687 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 4 Jul 2018 14:19:58 -0400 Subject: fix game freeze if the window loses focus while loading --- src/SMAPI/Framework/SGame.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index def0943c..240e9a97 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -223,17 +223,15 @@ namespace StardewModdingAPI.Framework return; } - // Load saves synchronously to avoid issues due to mod events triggering + // Run loaders synchronously to avoid issues due to mod events triggering // concurrently with game code. - if (Game1.gameMode == Game1.loadingMode) + if (Game1.currentLoader != null) { - this.Monitor.Log("Running game loader...", LogLevel.Trace); - while (Game1.gameMode == Game1.loadingMode) - { - base.Update(gameTime); - this.Events.Specialised_UnvalidatedUpdateTick.Raise(); - } - this.Monitor.Log("Game loader OK.", LogLevel.Trace); + this.Monitor.Log("Game loader synchronising...", LogLevel.Trace); + while (Game1.currentLoader?.MoveNext() == true) + continue; + Game1.currentLoader = null; + this.Monitor.Log("Game loader done.", LogLevel.Trace); } // While a background task is in progress, the game may make changes to the game -- cgit From 850cb505870a3426f8fde1bee498fdb8e29a0ee3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 4 Jul 2018 15:59:44 -0400 Subject: bump versions for beta release --- src/SMAPI/Constants.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index b9410153..be36bc84 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,10 +29,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19.2"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.22"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 2421fa3fa10b15ebf7bbe2e1893311c27a33b6fd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 6 Jul 2018 19:37:52 -0400 Subject: run new-day task synchronously This avoids issues when mod events are called asynchronously (like IAssetLoaders loading PNG tilesheets on season change while the game is drawing). --- src/SMAPI/Framework/SGame.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 240e9a97..d3865316 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -223,7 +224,7 @@ namespace StardewModdingAPI.Framework return; } - // Run loaders synchronously to avoid issues due to mod events triggering + // Run async tasks synchronously to avoid issues due to mod events triggering // concurrently with game code. if (Game1.currentLoader != null) { @@ -233,6 +234,12 @@ namespace StardewModdingAPI.Framework Game1.currentLoader = null; this.Monitor.Log("Game loader done.", LogLevel.Trace); } + if (Game1._newDayTask?.Status == TaskStatus.Created) + { + this.Monitor.Log("New day task synchronising...", LogLevel.Trace); + Game1._newDayTask.RunSynchronously(); + this.Monitor.Log("New day task done.", LogLevel.Trace); + } // While a background task is in progress, the game may make changes to the game // state while mods are running their code. This is risky, because data changes can -- cgit From 2b2860637d36b17d51ce279afaa4d81cefef289d Mon Sep 17 00:00:00 2001 From: Evan Behar Date: Fri, 6 Jul 2018 23:08:09 -0700 Subject: Linux-compatible scope resolution in validator --- .../Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index ecad649a..6364cec8 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -110,7 +110,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The type reference. private bool ShouldValidate(TypeReference type) { - return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); + // Extract scope name from type string representation for compatibility + // Under Linux, type.Scope.Name sometimes reports incorrectly + string scopeName = type.ToString(); + if (scopeName[0] != '$') + return false; + + scopeName = scopeName.Substring(0, scopeName.IndexOf(".", System.StringComparison.CurrentCulture)); + + return type != null && this.ValidateReferencesToAssemblies.Contains(scopeName); } /// Get a unique string representation of a type. -- cgit From 829e24b23e23ed44392c07d266107bf4a2f36998 Mon Sep 17 00:00:00 2001 From: "E. Behar" Date: Fri, 6 Jul 2018 23:21:13 -0700 Subject: Fix type==null case --- .../ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 6364cec8..bd5c97d6 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -110,6 +110,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The type reference. private bool ShouldValidate(TypeReference type) { + if (type == null) + return false; + // Extract scope name from type string representation for compatibility // Under Linux, type.Scope.Name sometimes reports incorrectly string scopeName = type.ToString(); @@ -118,7 +121,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders scopeName = scopeName.Substring(0, scopeName.IndexOf(".", System.StringComparison.CurrentCulture)); - return type != null && this.ValidateReferencesToAssemblies.Contains(scopeName); + return this.ValidateReferencesToAssemblies.Contains(scopeName); } /// Get a unique string representation of a type. -- cgit From 88f754e5b134f43ed6c7a833834aaeb92e44a62e Mon Sep 17 00:00:00 2001 From: Evan Behar Date: Sat, 7 Jul 2018 23:45:02 -0700 Subject: Expand validation to respect CIL placeholders --- .../ReferenceToMemberWithUnexpectedTypeFinder.cs | 7 +- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 163 +++++++++++++++++++++ 2 files changed, 167 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index bd5c97d6..79db6921 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -67,7 +67,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders // validate return type string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType); string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType); - if (actualReturnTypeID != expectedReturnTypeID) + + if (!RewriteHelper.LooksLikeSameType(expectedReturnTypeID, actualReturnTypeID)) { this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})"; return InstructionHandleResult.NotCompatible; @@ -110,8 +111,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The type reference. private bool ShouldValidate(TypeReference type) { - if (type == null) - return false; + if (type != null) + return true; // Extract scope name from type string representation for compatibility // Under Linux, type.Scope.Name sometimes reports incorrectly diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index 56a60a72..9eb8b3a5 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; @@ -90,5 +91,167 @@ namespace StardewModdingAPI.Framework.ModLoading .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); } + + /// Determine whether this type ID has a placeholder such as !0. + /// The type to check. + /// true if the type ID contains a placeholder, false if not. + public static bool HasPlaceholder(string typeID) + { + return typeID.Contains("!0"); + } + + /// returns whether this type ID is a placeholder, i.e., it begins with "!". + /// The symbol to validate. + /// true if the symbol is a placeholder, false if not + public static bool IsPlaceholder(string symbol) + { + return symbol.StartsWith("!"); + } + + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. + /// The type ID to compare. + /// The other type ID to compare. + /// true if the type IDs look like the same type, false if not. + public static bool LooksLikeSameType(string typeA, string typeB) + { + string placeholderType, actualType = ""; + + if (RewriteHelper.HasPlaceholder(typeA)) + { + placeholderType = typeA; + actualType = typeB; + } else if (RewriteHelper.HasPlaceholder(typeB)) + { + placeholderType = typeB; + actualType = typeA; + } else + { + return typeA == typeB; + } + + return RewriteHelper.PlaceholderTypeValidates(placeholderType, actualType); + } + + protected class SymbolLocation + { + public string symbol; + public int depth; + + public SymbolLocation(string symbol, int depth) + { + this.symbol = symbol; + this.depth = depth; + } + } + + private static List symbolBoundaries = new List{'<', '>', ','}; + + /// Traverses and parses out symbols from a type which does not contain placeholder values. + /// The type to traverse. + /// A List in which to store the parsed symbols. + private static void TraverseActualType(string type, List typeSymbols) + { + int depth = 0; + string symbol = ""; + + foreach (char c in type) + { + if (RewriteHelper.symbolBoundaries.Contains(c)) + { + typeSymbols.Add(new SymbolLocation(symbol, depth)); + symbol = ""; + switch (c) { + case '<': + depth++; + break; + case '>': + depth--; + break; + default: + break; + } + } + else + symbol += c; + } + } + + /// Determines whether two symbols in a type ID match, accounting for placeholders such as !0. + /// A symbol in a typename which contains placeholders. + /// A symbol in a typename which does not contain placeholders. + /// A dictionary containing a mapping of placeholders to concrete types. + /// true if the symbols match, false if not. + private static bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) + { + System.Console.Write($"comparing {symbolA.symbol} at depth {symbolA.depth} to {symbolB.symbol} at {symbolB.depth}"); + if (symbolA.depth != symbolB.depth) + return false; + + if (!RewriteHelper.IsPlaceholder(symbolA.symbol)) { + return symbolA.symbol == symbolB.symbol; + } + + if (placeholderMap.ContainsKey(symbolA.symbol)) + { + return placeholderMap[symbolA.symbol] == symbolB.symbol; + } + + placeholderMap[symbolA.symbol] = symbolB.symbol; + + return true; + } + + /// Determines whether a type which has placeholders correctly resolves to the concrete type provided. + /// A type containing placeholders such as !0. + /// The list of symbols extracted from the concrete type. + /// true if the type resolves correctly, false if not. + private static bool PlaceholderTypeResolvesToActualType(string type, List typeSymbols) + { + Dictionary placeholderMap = new Dictionary(); + + int depth = 0, symbolCount = 0; + string symbol = ""; + + foreach (char c in type) + { + if (symbolBoundaries.Contains(c)) + { + bool match = RewriteHelper.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); + System.Console.Write($"match: {match}"); + if (typeSymbols.Count <= symbolCount || + !match) + return false; + + symbolCount++; + symbol = ""; + switch (c) + { + case '<': + depth++; + break; + case '>': + depth--; + break; + default: + break; + } + } + else + symbol += c; + } + + return true; + } + + /// Determines whether a type with placeholders in it matches a type without placeholders. + /// The type with placeholders in it. + /// The type without placeholders. + /// true if the placeholder type can resolve to the actual type, false if not. + private static bool PlaceholderTypeValidates(string placeholderType, string actualType) { + List typeSymbols = new List(); + + RewriteHelper.TraverseActualType(actualType, typeSymbols); + return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols); + } } } -- cgit From a30794894bd9bd3e152c882286f0f3600ea41400 Mon Sep 17 00:00:00 2001 From: Evan Behar Date: Sat, 7 Jul 2018 23:46:28 -0700 Subject: Revert ShouldValidate --- .../Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 79db6921..88ba36ee 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -111,18 +111,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The type reference. private bool ShouldValidate(TypeReference type) { - if (type != null) - return true; - - // Extract scope name from type string representation for compatibility - // Under Linux, type.Scope.Name sometimes reports incorrectly - string scopeName = type.ToString(); - if (scopeName[0] != '$') - return false; - - scopeName = scopeName.Substring(0, scopeName.IndexOf(".", System.StringComparison.CurrentCulture)); - - return this.ValidateReferencesToAssemblies.Contains(scopeName); + return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); } /// Get a unique string representation of a type. -- cgit From 1a3810d722c89749dadbf2bc260ff7a886e08368 Mon Sep 17 00:00:00 2001 From: "E. Behar" Date: Sat, 7 Jul 2018 16:50:01 -0700 Subject: Remove extraneous debug output --- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index 9eb8b3a5..fcbb7063 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -183,7 +183,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// true if the symbols match, false if not. private static bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) { - System.Console.Write($"comparing {symbolA.symbol} at depth {symbolA.depth} to {symbolB.symbol} at {symbolB.depth}"); if (symbolA.depth != symbolB.depth) return false; -- cgit From 1dfcbc61736de1d3631cf0571b945e0c4dfc50f5 Mon Sep 17 00:00:00 2001 From: "E. Behar" Date: Sat, 7 Jul 2018 16:51:03 -0700 Subject: Remove another extraneous debug output. =_= --- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index fcbb7063..74498a3e 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -216,7 +216,6 @@ namespace StardewModdingAPI.Framework.ModLoading if (symbolBoundaries.Contains(c)) { bool match = RewriteHelper.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); - System.Console.Write($"match: {match}"); if (typeSymbols.Count <= symbolCount || !match) return false; -- cgit From f6254e17ead217cf7b2488a9b2d5b8d0bc9b23d8 Mon Sep 17 00:00:00 2001 From: "E. Behar" Date: Sun, 8 Jul 2018 10:22:23 -0700 Subject: Fix missing assignment. --- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index 74498a3e..1600069d 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -114,7 +114,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// true if the type IDs look like the same type, false if not. public static bool LooksLikeSameType(string typeA, string typeB) { - string placeholderType, actualType = ""; + string placeholderType = "", actualType = ""; if (RewriteHelper.HasPlaceholder(typeA)) { -- cgit From 40fbafdb73d0501f5239d3b857b6cb3bf2929bab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 13:26:07 -0400 Subject: fix new logic not applied to method return types --- .../ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 88ba36ee..47c8b33c 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -93,7 +93,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType); - if (candidateMethods.All(method => this.GetComparableTypeID(method.ReturnType) != expectedReturnType)) + if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(this.GetComparableTypeID(method.ReturnType), expectedReturnType))) { this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})"; return InstructionHandleResult.NotCompatible; -- cgit From befeafd31d7a3351cb138c210b26f126716d05f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 13:56:46 -0400 Subject: encapsulate GetComparableTypeID --- .../ReferenceToMemberWithUnexpectedTypeFinder.cs | 30 ++++----------- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 43 ++++++++++++++++------ 2 files changed, 39 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 47c8b33c..cf5a3175 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using Mono.Cecil; using Mono.Cecil.Cil; @@ -16,9 +15,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The assembly names to which to heuristically detect broken references. private readonly HashSet ValidateReferencesToAssemblies; - /// A pattern matching type name substrings to strip for display. - private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); - /********* ** Accessors @@ -65,12 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders return InstructionHandleResult.None; // validate return type - string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType); - string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType); - - if (!RewriteHelper.LooksLikeSameType(expectedReturnTypeID, actualReturnTypeID)) + if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) { - this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})"; + this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"; return InstructionHandleResult.NotCompatible; } } @@ -92,10 +85,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders return InstructionHandleResult.NotCompatible; } - string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType); - if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(this.GetComparableTypeID(method.ReturnType), expectedReturnType))) + if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) { - this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})"; + this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"; return InstructionHandleResult.NotCompatible; } } @@ -114,17 +106,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); } - /// Get a unique string representation of a type. - /// The type reference. - private string GetComparableTypeID(TypeReference type) - { - return this.StripTypeNamePattern.Replace(type.FullName, ""); - } - /// Get a shorter type name for display. /// The type reference. - /// The comparable type ID from . - private string GetFriendlyTypeName(TypeReference type, string typeID) + private string GetFriendlyTypeName(TypeReference type) { // most common built-in types switch (type.FullName) @@ -141,10 +125,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" }) { if (type.Namespace == @namespace) - return typeID.Substring(@namespace.Length + 1); + return type.Name; } - return typeID; + return type.FullName; } } } diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index 1600069d..f8684cde 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Mono.Cecil; using Mono.Cecil.Cil; @@ -10,6 +11,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// Provides helper methods for field rewriters. internal static class RewriteHelper { + /********* + ** Properties + *********/ + /// A pattern matching type name substrings to strip for display. + private static readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); + + /********* ** Public methods *********/ @@ -109,29 +117,39 @@ namespace StardewModdingAPI.Framework.ModLoading } /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. - /// The type ID to compare. - /// The other type ID to compare. + /// The type ID to compare. + /// The other type ID to compare. /// true if the type IDs look like the same type, false if not. - public static bool LooksLikeSameType(string typeA, string typeB) + public static bool LooksLikeSameType(TypeReference a, TypeReference b) { + string typeA = RewriteHelper.GetComparableTypeID(a); + string typeB = RewriteHelper.GetComparableTypeID(b); + string placeholderType = "", actualType = ""; if (RewriteHelper.HasPlaceholder(typeA)) { placeholderType = typeA; actualType = typeB; - } else if (RewriteHelper.HasPlaceholder(typeB)) + } + else if (RewriteHelper.HasPlaceholder(typeB)) { placeholderType = typeB; actualType = typeA; - } else - { - return typeA == typeB; } + else + return typeA == typeB; return RewriteHelper.PlaceholderTypeValidates(placeholderType, actualType); } + /// Get a unique string representation of a type. + /// The type reference. + private static string GetComparableTypeID(TypeReference type) + { + return RewriteHelper.StripTypeNamePattern.Replace(type.FullName, ""); + } + protected class SymbolLocation { public string symbol; @@ -144,7 +162,7 @@ namespace StardewModdingAPI.Framework.ModLoading } } - private static List symbolBoundaries = new List{'<', '>', ','}; + private static List symbolBoundaries = new List { '<', '>', ',' }; /// Traverses and parses out symbols from a type which does not contain placeholder values. /// The type to traverse. @@ -160,7 +178,8 @@ namespace StardewModdingAPI.Framework.ModLoading { typeSymbols.Add(new SymbolLocation(symbol, depth)); symbol = ""; - switch (c) { + switch (c) + { case '<': depth++; break; @@ -186,7 +205,8 @@ namespace StardewModdingAPI.Framework.ModLoading if (symbolA.depth != symbolB.depth) return false; - if (!RewriteHelper.IsPlaceholder(symbolA.symbol)) { + if (!RewriteHelper.IsPlaceholder(symbolA.symbol)) + { return symbolA.symbol == symbolB.symbol; } @@ -245,7 +265,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The type with placeholders in it. /// The type without placeholders. /// true if the placeholder type can resolve to the actual type, false if not. - private static bool PlaceholderTypeValidates(string placeholderType, string actualType) { + private static bool PlaceholderTypeValidates(string placeholderType, string actualType) + { List typeSymbols = new List(); RewriteHelper.TraverseActualType(actualType, typeSymbols); -- cgit From 0079110870e4944e734be507ede91e7b0b655df6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 13:58:37 -0400 Subject: encapsulate type reference comparison --- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 188 ++---------------- .../Framework/ModLoading/TypeReferenceComparer.cs | 209 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 221 insertions(+), 177 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index f8684cde..2f79809c 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using Mono.Cecil; using Mono.Cecil.Cil; @@ -14,8 +12,8 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Properties *********/ - /// A pattern matching type name substrings to strip for display. - private static readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); + /// The comparer which heuristically compares type definitions. + private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer(); /********* @@ -68,6 +66,15 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. + /// The type ID to compare. + /// The other type ID to compare. + /// true if the type IDs look like the same type, false if not. + public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB) + { + return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB); + } + /// Get whether a method definition matches the signature expected by a method reference. /// The method definition. /// The method reference. @@ -99,178 +106,5 @@ namespace StardewModdingAPI.Framework.ModLoading .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); } - - /// Determine whether this type ID has a placeholder such as !0. - /// The type to check. - /// true if the type ID contains a placeholder, false if not. - public static bool HasPlaceholder(string typeID) - { - return typeID.Contains("!0"); - } - - /// returns whether this type ID is a placeholder, i.e., it begins with "!". - /// The symbol to validate. - /// true if the symbol is a placeholder, false if not - public static bool IsPlaceholder(string symbol) - { - return symbol.StartsWith("!"); - } - - /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. - /// The type ID to compare. - /// The other type ID to compare. - /// true if the type IDs look like the same type, false if not. - public static bool LooksLikeSameType(TypeReference a, TypeReference b) - { - string typeA = RewriteHelper.GetComparableTypeID(a); - string typeB = RewriteHelper.GetComparableTypeID(b); - - string placeholderType = "", actualType = ""; - - if (RewriteHelper.HasPlaceholder(typeA)) - { - placeholderType = typeA; - actualType = typeB; - } - else if (RewriteHelper.HasPlaceholder(typeB)) - { - placeholderType = typeB; - actualType = typeA; - } - else - return typeA == typeB; - - return RewriteHelper.PlaceholderTypeValidates(placeholderType, actualType); - } - - /// Get a unique string representation of a type. - /// The type reference. - private static string GetComparableTypeID(TypeReference type) - { - return RewriteHelper.StripTypeNamePattern.Replace(type.FullName, ""); - } - - protected class SymbolLocation - { - public string symbol; - public int depth; - - public SymbolLocation(string symbol, int depth) - { - this.symbol = symbol; - this.depth = depth; - } - } - - private static List symbolBoundaries = new List { '<', '>', ',' }; - - /// Traverses and parses out symbols from a type which does not contain placeholder values. - /// The type to traverse. - /// A List in which to store the parsed symbols. - private static void TraverseActualType(string type, List typeSymbols) - { - int depth = 0; - string symbol = ""; - - foreach (char c in type) - { - if (RewriteHelper.symbolBoundaries.Contains(c)) - { - typeSymbols.Add(new SymbolLocation(symbol, depth)); - symbol = ""; - switch (c) - { - case '<': - depth++; - break; - case '>': - depth--; - break; - default: - break; - } - } - else - symbol += c; - } - } - - /// Determines whether two symbols in a type ID match, accounting for placeholders such as !0. - /// A symbol in a typename which contains placeholders. - /// A symbol in a typename which does not contain placeholders. - /// A dictionary containing a mapping of placeholders to concrete types. - /// true if the symbols match, false if not. - private static bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) - { - if (symbolA.depth != symbolB.depth) - return false; - - if (!RewriteHelper.IsPlaceholder(symbolA.symbol)) - { - return symbolA.symbol == symbolB.symbol; - } - - if (placeholderMap.ContainsKey(symbolA.symbol)) - { - return placeholderMap[symbolA.symbol] == symbolB.symbol; - } - - placeholderMap[symbolA.symbol] = symbolB.symbol; - - return true; - } - - /// Determines whether a type which has placeholders correctly resolves to the concrete type provided. - /// A type containing placeholders such as !0. - /// The list of symbols extracted from the concrete type. - /// true if the type resolves correctly, false if not. - private static bool PlaceholderTypeResolvesToActualType(string type, List typeSymbols) - { - Dictionary placeholderMap = new Dictionary(); - - int depth = 0, symbolCount = 0; - string symbol = ""; - - foreach (char c in type) - { - if (symbolBoundaries.Contains(c)) - { - bool match = RewriteHelper.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); - if (typeSymbols.Count <= symbolCount || - !match) - return false; - - symbolCount++; - symbol = ""; - switch (c) - { - case '<': - depth++; - break; - case '>': - depth--; - break; - default: - break; - } - } - else - symbol += c; - } - - return true; - } - - /// Determines whether a type with placeholders in it matches a type without placeholders. - /// The type with placeholders in it. - /// The type without placeholders. - /// true if the placeholder type can resolve to the actual type, false if not. - private static bool PlaceholderTypeValidates(string placeholderType, string actualType) - { - List typeSymbols = new List(); - - RewriteHelper.TraverseActualType(actualType, typeSymbols); - return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols); - } } } diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs new file mode 100644 index 00000000..8d128b37 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Performs heuristic equality checks for instances. + internal class TypeReferenceComparer : IEqualityComparer + { + /********* + ** Properties + *********/ + /// A pattern matching type name substrings to strip for display. + private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); + + private List symbolBoundaries = new List { '<', '>', ',' }; + + + /********* + ** Public methods + *********/ + /// Get whether the specified objects are equal. + /// The first object to compare. + /// The second object to compare. + public bool Equals(TypeReference a, TypeReference b) + { + string typeA = this.GetComparableTypeID(a); + string typeB = this.GetComparableTypeID(b); + + string placeholderType = "", actualType = ""; + + if (this.HasPlaceholder(typeA)) + { + placeholderType = typeA; + actualType = typeB; + } + else if (this.HasPlaceholder(typeB)) + { + placeholderType = typeB; + actualType = typeA; + } + else + return typeA == typeB; + + return this.PlaceholderTypeValidates(placeholderType, actualType); + } + + /// Get a hash code for the specified object. + /// The object for which a hash code is to be returned. + /// The object type is a reference type and is null. + public int GetHashCode(TypeReference obj) + { + return obj.GetHashCode(); + } + + + /********* + ** Private methods + *********/ + /// Get a unique string representation of a type. + /// The type reference. + private string GetComparableTypeID(TypeReference type) + { + return this.StripTypeNamePattern.Replace(type.FullName, ""); + } + + /// Determine whether this type ID has a placeholder such as !0. + /// The type to check. + /// true if the type ID contains a placeholder, false if not. + private bool HasPlaceholder(string typeID) + { + return typeID.Contains("!0"); + } + + /// returns whether this type ID is a placeholder, i.e., it begins with "!". + /// The symbol to validate. + /// true if the symbol is a placeholder, false if not + private bool IsPlaceholder(string symbol) + { + return symbol.StartsWith("!"); + } + + /// Traverses and parses out symbols from a type which does not contain placeholder values. + /// The type to traverse. + /// A List in which to store the parsed symbols. + private void TraverseActualType(string type, List typeSymbols) + { + int depth = 0; + string symbol = ""; + + foreach (char c in type) + { + if (this.symbolBoundaries.Contains(c)) + { + typeSymbols.Add(new SymbolLocation(symbol, depth)); + symbol = ""; + switch (c) + { + case '<': + depth++; + break; + case '>': + depth--; + break; + default: + break; + } + } + else + symbol += c; + } + } + + /// Determines whether two symbols in a type ID match, accounting for placeholders such as !0. + /// A symbol in a typename which contains placeholders. + /// A symbol in a typename which does not contain placeholders. + /// A dictionary containing a mapping of placeholders to concrete types. + /// true if the symbols match, false if not. + private bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) + { + if (symbolA.depth != symbolB.depth) + return false; + + if (!this.IsPlaceholder(symbolA.symbol)) + { + return symbolA.symbol == symbolB.symbol; + } + + if (placeholderMap.ContainsKey(symbolA.symbol)) + { + return placeholderMap[symbolA.symbol] == symbolB.symbol; + } + + placeholderMap[symbolA.symbol] = symbolB.symbol; + + return true; + } + + /// Determines whether a type which has placeholders correctly resolves to the concrete type provided. + /// A type containing placeholders such as !0. + /// The list of symbols extracted from the concrete type. + /// true if the type resolves correctly, false if not. + private bool PlaceholderTypeResolvesToActualType(string type, List typeSymbols) + { + Dictionary placeholderMap = new Dictionary(); + + int depth = 0, symbolCount = 0; + string symbol = ""; + + foreach (char c in type) + { + if (this.symbolBoundaries.Contains(c)) + { + bool match = this.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); + if (typeSymbols.Count <= symbolCount || + !match) + return false; + + symbolCount++; + symbol = ""; + switch (c) + { + case '<': + depth++; + break; + case '>': + depth--; + break; + default: + break; + } + } + else + symbol += c; + } + + return true; + } + + /// Determines whether a type with placeholders in it matches a type without placeholders. + /// The type with placeholders in it. + /// The type without placeholders. + /// true if the placeholder type can resolve to the actual type, false if not. + private bool PlaceholderTypeValidates(string placeholderType, string actualType) + { + List typeSymbols = new List(); + + this.TraverseActualType(actualType, typeSymbols); + return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols); + } + + + + /********* + ** Inner classes + *********/ + protected class SymbolLocation + { + public string symbol; + public int depth; + + public SymbolLocation(string symbol, int depth) + { + this.symbol = symbol; + this.depth = depth; + } + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c13f5e30..57c2c9e8 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,6 +110,7 @@ + -- cgit From 530b120014c0ae7fc2994b21fc388ea36ddb4ce8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 15:48:32 -0400 Subject: rewrite TypeReference comparison to handle more edge cases, exit earlier if possible, and encapsulate a bit more --- .../Framework/ModLoading/TypeReferenceComparer.cs | 304 ++++++++++----------- 1 file changed, 148 insertions(+), 156 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs index 8d128b37..f7497789 100644 --- a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs +++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs @@ -1,21 +1,23 @@ +using System; using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Linq; using Mono.Cecil; namespace StardewModdingAPI.Framework.ModLoading { /// Performs heuristic equality checks for instances. + /// + /// This implementation compares instances to see if they likely + /// refer to the same type. While the implementation is obvious for types like System.Bool, + /// this class mainly exists to handle cases like System.Collections.Generic.Dictionary`2<!0,Netcode.NetRoot`1<!1>> + /// and System.Collections.Generic.Dictionary`2<TKey,Netcode.NetRoot`1<TValue>> + /// which are compatible, but not directly comparable. It does this by splitting each type name + /// into its component token types, and performing placeholder substitution (e.g. !0 to + /// TKey in the above example). If all components are equal after substitution, and the + /// tokens can all be mapped to the same generic type, the types are considered equal. + /// internal class TypeReferenceComparer : IEqualityComparer { - /********* - ** Properties - *********/ - /// A pattern matching type name substrings to strip for display. - private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); - - private List symbolBoundaries = new List { '<', '>', ',' }; - - /********* ** Public methods *********/ @@ -24,25 +26,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The second object to compare. public bool Equals(TypeReference a, TypeReference b) { - string typeA = this.GetComparableTypeID(a); - string typeB = this.GetComparableTypeID(b); - - string placeholderType = "", actualType = ""; + if (a == null || b == null) + return a == b; - if (this.HasPlaceholder(typeA)) - { - placeholderType = typeA; - actualType = typeB; - } - else if (this.HasPlaceholder(typeB)) - { - placeholderType = typeB; - actualType = typeA; - } - else - return typeA == typeB; - - return this.PlaceholderTypeValidates(placeholderType, actualType); + return + a == b + || a.FullName == b.FullName + || this.HeuristicallyEquals(a, b); } /// Get a hash code for the specified object. @@ -57,153 +47,155 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Private methods *********/ - /// Get a unique string representation of a type. - /// The type reference. - private string GetComparableTypeID(TypeReference type) + /// Get whether two types are heuristically equal based on generic type token substitution. + /// The first type to compare. + /// The second type to compare. + private bool HeuristicallyEquals(TypeReference typeA, TypeReference typeB) { - return this.StripTypeNamePattern.Replace(type.FullName, ""); - } - - /// Determine whether this type ID has a placeholder such as !0. - /// The type to check. - /// true if the type ID contains a placeholder, false if not. - private bool HasPlaceholder(string typeID) - { - return typeID.Contains("!0"); - } - - /// returns whether this type ID is a placeholder, i.e., it begins with "!". - /// The symbol to validate. - /// true if the symbol is a placeholder, false if not - private bool IsPlaceholder(string symbol) - { - return symbol.StartsWith("!"); - } - - /// Traverses and parses out symbols from a type which does not contain placeholder values. - /// The type to traverse. - /// A List in which to store the parsed symbols. - private void TraverseActualType(string type, List typeSymbols) - { - int depth = 0; - string symbol = ""; - - foreach (char c in type) + bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary tokenMap) { - if (this.symbolBoundaries.Contains(c)) + // analyse type names + bool hasTokensA = typeNameA.Contains("!"); + bool hasTokensB = typeNameB.Contains("!"); + bool isTokenA = hasTokensA && typeNameA[0] == '!'; + bool isTokenB = hasTokensB && typeNameB[0] == '!'; + + // validate + if (!hasTokensA && !hasTokensB) + return typeNameA == typeNameB; // no substitution needed + if (hasTokensA && hasTokensB) + throw new InvalidOperationException("Can't compare two type names when both contain generic type tokens."); + + // perform substitution if applicable + if (isTokenA) + typeNameA = this.MapPlaceholder(placeholder: typeNameA, type: typeNameB, map: tokenMap); + if (isTokenB) + typeNameB = this.MapPlaceholder(placeholder: typeNameB, type: typeNameA, map: tokenMap); + + // compare inner tokens + string[] symbolsA = this.GetTypeSymbols(typeNameA).ToArray(); + string[] symbolsB = this.GetTypeSymbols(typeNameB).ToArray(); + if (symbolsA.Length != symbolsB.Length) + return false; + + for (int i = 0; i < symbolsA.Length; i++) { - typeSymbols.Add(new SymbolLocation(symbol, depth)); - symbol = ""; - switch (c) - { - case '<': - depth++; - break; - case '>': - depth--; - break; - default: - break; - } + if (!HeuristicallyEquals(symbolsA[i], symbolsB[i], tokenMap)) + return false; } - else - symbol += c; - } - } - /// Determines whether two symbols in a type ID match, accounting for placeholders such as !0. - /// A symbol in a typename which contains placeholders. - /// A symbol in a typename which does not contain placeholders. - /// A dictionary containing a mapping of placeholders to concrete types. - /// true if the symbols match, false if not. - private bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary placeholderMap) - { - if (symbolA.depth != symbolB.depth) - return false; - - if (!this.IsPlaceholder(symbolA.symbol)) - { - return symbolA.symbol == symbolB.symbol; + return true; } - if (placeholderMap.ContainsKey(symbolA.symbol)) - { - return placeholderMap[symbolA.symbol] == symbolB.symbol; - } + return HeuristicallyEquals(typeA.FullName, typeB.FullName, new Dictionary()); + } - placeholderMap[symbolA.symbol] = symbolB.symbol; + /// Map a generic type placeholder (like !0) to its actual type. + /// The token placeholder. + /// The actual type. + /// The map of token to map substitutions. + /// Returns the previously-mapped type if applicable, else the . + private string MapPlaceholder(string placeholder, string type, IDictionary map) + { + if (map.TryGetValue(placeholder, out string result)) + return result; - return true; + map[placeholder] = type; + return type; } - /// Determines whether a type which has placeholders correctly resolves to the concrete type provided. - /// A type containing placeholders such as !0. - /// The list of symbols extracted from the concrete type. - /// true if the type resolves correctly, false if not. - private bool PlaceholderTypeResolvesToActualType(string type, List typeSymbols) + /// Get the top-level type symbols in a type name (e.g. List and NetRef<T> in List<NetRef<T>>) + /// The full type name. + private IEnumerable GetTypeSymbols(string typeName) { - Dictionary placeholderMap = new Dictionary(); + int openGenerics = 0; - int depth = 0, symbolCount = 0; + Queue queue = new Queue(typeName); string symbol = ""; - - foreach (char c in type) + while (queue.Any()) { - if (this.symbolBoundaries.Contains(c)) + char ch = queue.Dequeue(); + switch (ch) { - bool match = this.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap); - if (typeSymbols.Count <= symbolCount || - !match) - return false; - - symbolCount++; - symbol = ""; - switch (c) - { - case '<': - depth++; - break; - case '>': - depth--; - break; - default: - break; - } + // skip `1 generic type identifiers + case '`': + while (int.TryParse(queue.Peek().ToString(), out int _)) + queue.Dequeue(); + break; + + // start generic args + case '<': + switch (openGenerics) + { + // start new generic symbol + case 0: + yield return symbol; + symbol = ""; + openGenerics++; + break; + + // continue accumulating nested type symbol + default: + symbol += ch; + openGenerics++; + break; + } + break; + + // generic args delimiter + case ',': + switch (openGenerics) + { + // invalid + case 0: + throw new InvalidOperationException($"Encountered unexpected comma in type name: {typeName}."); + + // start next generic symbol + case 1: + yield return symbol; + symbol = ""; + break; + + // continue accumulating nested type symbol + default: + symbol += ch; + break; + } + break; + + + // end generic args + case '>': + switch (openGenerics) + { + // invalid + case 0: + throw new InvalidOperationException($"Encountered unexpected closing generic in type name: {typeName}."); + + // end generic symbol + case 1: + yield return symbol; + symbol = ""; + openGenerics--; + break; + + // continue accumulating nested type symbol + default: + symbol += ch; + openGenerics--; + break; + } + break; + + // continue symbol + default: + symbol += ch; + break; } - else - symbol += c; } - return true; - } - - /// Determines whether a type with placeholders in it matches a type without placeholders. - /// The type with placeholders in it. - /// The type without placeholders. - /// true if the placeholder type can resolve to the actual type, false if not. - private bool PlaceholderTypeValidates(string placeholderType, string actualType) - { - List typeSymbols = new List(); - - this.TraverseActualType(actualType, typeSymbols); - return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols); - } - - - - /********* - ** Inner classes - *********/ - protected class SymbolLocation - { - public string symbol; - public int depth; - - public SymbolLocation(string symbol, int depth) - { - this.symbol = symbol; - this.depth = depth; - } + if (symbol != "") + yield return symbol; } } } -- cgit From 8839b6822a475c693a0b4a88c6de861028df3caf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 15:56:08 -0400 Subject: also detect broken Netcode references --- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index aa3e743c..2f0c1b15 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI.Metadata *********/ /// The assembly names to which to heuristically detect broken references. /// The current implementation only works correctly with assemblies that should always be present. - private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley" }; + private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley", "Netcode" }; /********* -- cgit From 7e46cc24630d810f4e2396346124780160cb7aa3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 18:54:34 -0400 Subject: update Patreon list --- src/SMAPI.Web/Views/Index/Index.cshtml | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 411448fa..361d01de 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -94,6 +94,7 @@ else jwdred, KNakamura, Kono Tyran, + Pucklynn, Robby LaFarge, and a few anonymous users for their ongoing support; you're awesome! 🏅

    -- cgit From 3b078d55daccd13332e2cba1fd5c76f775505d7a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 20:06:33 -0400 Subject: add GameLoop events for SMAPI 3.0 (#310) --- src/SMAPI/Events/GameLoopLaunchedEventArgs.cs | 7 ++++ src/SMAPI/Events/GameLoopUpdatedEventArgs.cs | 36 ++++++++++++++++++++ src/SMAPI/Events/GameLoopUpdatingEventArgs.cs | 36 ++++++++++++++++++++ src/SMAPI/Events/IGameLoopEvents.cs | 17 ++++++++++ src/SMAPI/Events/IModEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 45 ++++++++++++++++--------- src/SMAPI/Framework/Events/ModEvents.cs | 4 +++ src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 39 +++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 17 +++++----- src/SMAPI/StardewModdingAPI.csproj | 5 +++ 10 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/SMAPI/Events/GameLoopLaunchedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLoopUpdatedEventArgs.cs create mode 100644 src/SMAPI/Events/GameLoopUpdatingEventArgs.cs create mode 100644 src/SMAPI/Events/IGameLoopEvents.cs create mode 100644 src/SMAPI/Framework/Events/ModGameLoopEvents.cs (limited to 'src') diff --git a/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs new file mode 100644 index 00000000..6a42e4f9 --- /dev/null +++ b/src/SMAPI/Events/GameLoopLaunchedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopLaunchedEventArgs : EventArgs { } +} diff --git a/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs new file mode 100644 index 00000000..3ad34b69 --- /dev/null +++ b/src/SMAPI/Events/GameLoopUpdatedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopUpdatedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public GameLoopUpdatedEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs new file mode 100644 index 00000000..d6a8b5c2 --- /dev/null +++ b/src/SMAPI/Events/GameLoopUpdatingEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLoopUpdatingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks { get; } + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The number of ticks elapsed since the game started, including the current tick. + public GameLoopUpdatingEventArgs(uint ticks) + { + this.Ticks = ticks; + this.IsOneSecond = this.IsMultipleOf(60); + } + + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs new file mode 100644 index 00000000..a56b3de3 --- /dev/null +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public interface IGameLoopEvents + { + /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations. + event EventHandler Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + event EventHandler Updating; + + /// Raised after the game performs its overall update tick (≈60 times per second). + event EventHandler Updated; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 16ec6557..cf2f8cb8 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events /// Manages access to events raised by SMAPI. public interface IModEvents { + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + IGameLoopEvents GameLoop { get; } + /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index b05d82ce..3d5d0124 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -11,6 +11,33 @@ namespace StardewModdingAPI.Framework.Events /********* ** Events (new) *********/ + /**** + ** Game loop + ****/ + /// Raised after the game is launched, right before the first update tick. + public readonly ManagedEvent GameLoop_Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent GameLoop_Updating; + + /// Raised after the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent GameLoop_Updated; + + /**** + ** Input + ****/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised after the player released a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + + /// Raised after the player moves the in-game cursor. + public readonly ManagedEvent Input_CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + public readonly ManagedEvent Input_MouseWheelScrolled; + /**** ** World ****/ @@ -35,21 +62,6 @@ namespace StardewModdingAPI.Framework.Events /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent World_TerrainFeatureListChanged; - /**** - ** Input - ****/ - /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; - - /// Raised after the player released a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; - - /// Raised after the player moves the in-game cursor. - public readonly ManagedEvent Input_CursorMoved; - - /// Raised after the player scrolls the mouse wheel. - public readonly ManagedEvent Input_MouseWheelScrolled; - /********* ** Events (old) @@ -252,6 +264,9 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.GameLoop_Updating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating)); + this.GameLoop_Updated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated)); + this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 90853141..9e474457 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public IGameLoopEvents GameLoop { get; } + /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } @@ -23,6 +26,7 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event manager. public ModEvents(IModMetadata mod, EventManager eventManager) { + this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs new file mode 100644 index 00000000..1a142b0f --- /dev/null +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -0,0 +1,39 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + internal class ModGameLoopEvents : ModEventsBase, IGameLoopEvents + { + /********* + ** Accessors + *********/ + /// Raised after the game is launched, right before the first update tick. + public event EventHandler Launched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public event EventHandler Updating + { + add => this.EventManager.GameLoop_Updating.Add(value); + remove => this.EventManager.GameLoop_Updating.Remove(value); + } + + /// Raised after the game performs its overall update tick (≈60 times per second). + public event EventHandler Updated + { + add => this.EventManager.GameLoop_Updated.Add(value); + remove => this.EventManager.GameLoop_Updated.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModGameLoopEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d3865316..777bc478 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -94,8 +94,8 @@ namespace StardewModdingAPI.Framework /// Whether post-game-startup initialisation has been performed. private bool IsInitialised; - /// Whether this is the very first update tick since the game started. - private bool FirstUpdate; + /// The number of update ticks which have already executed. + private uint TicksElapsed = 0; /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; @@ -138,7 +138,6 @@ namespace StardewModdingAPI.Framework // init SMAPI this.Monitor = monitor; this.Events = eventManager; - this.FirstUpdate = true; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; @@ -669,25 +668,27 @@ namespace StardewModdingAPI.Framework /********* ** Game update *********/ - this.Input.UpdateSuppression(); + this.TicksElapsed++; + if (this.TicksElapsed == 1) + this.Events.GameLoop_Launched.Raise(new GameLoopLaunchedEventArgs()); + this.Events.GameLoop_Updating.Raise(new GameLoopUpdatingEventArgs(this.TicksElapsed)); try { + this.Input.UpdateSuppression(); base.Update(gameTime); } catch (Exception ex) { this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } + this.Events.GameLoop_Updated.Raise(new GameLoopUpdatedEventArgs(this.TicksElapsed)); /********* ** Update events *********/ this.Events.Specialised_UnvalidatedUpdateTick.Raise(); - if (this.FirstUpdate) - { - this.FirstUpdate = false; + if (this.TicksElapsed == 1) this.Events.Game_FirstUpdateTick.Raise(); - } this.Events.Game_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) this.Events.Game_SecondUpdateTick.Raise(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 57c2c9e8..27e98f3a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -90,15 +90,19 @@ Properties\GlobalAssemblyInfo.cs + + + + @@ -125,6 +129,7 @@ + -- cgit From 0f6f6c65c211f94ad7842e8a9d024471ddf933d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 20:09:31 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index dc5ea3f2..83c1b32d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.19.1", + "Version": "2.6.0-beta.19.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index eb2f0abc..6bc8cf8b 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.19.1", + "Version": "2.6.0-beta.19.3", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index be36bc84..3e0f51e7 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,10 +29,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19.2"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19.3"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.22"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.23"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 68a21ff249c8a6cd9bdcece64e53162831fe7fd9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Jul 2018 20:50:42 -0400 Subject: fix new event not initialised (#310) --- src/SMAPI/Framework/Events/EventManager.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 3d5d0124..4de333a3 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -264,6 +264,7 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) + this.GameLoop_Launched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Launched)); this.GameLoop_Updating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating)); this.GameLoop_Updated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated)); -- cgit From fbf8356452db76f902578072aab3d757497e4cd1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 00:35:09 -0400 Subject: fix new event (#310) --- src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 1a142b0f..379a4e96 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -10,7 +10,11 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after the game is launched, right before the first update tick. - public event EventHandler Launched; + public event EventHandler Launched + { + add => this.EventManager.GameLoop_Launched.Add(value); + remove => this.EventManager.GameLoop_Launched.Remove(value); + } /// Raised before the game performs its overall update tick (≈60 times per second). public event EventHandler Updating -- cgit From 1fd52f8b63d2369de84b7ab8d54596d2cb597abf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 00:58:11 -0400 Subject: detect broken constructor references --- .../ModLoading/Finders/ReferenceToMissingMemberFinder.cs | 11 +++++++---- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index f5e33313..b95dd79c 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -67,12 +67,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef)) { - MethodDefinition target = methodRef.DeclaringType.Resolve()?.Methods.FirstOrDefault(p => p.Name == methodRef.Name); + MethodDefinition target = methodRef.Resolve(); if (target == null) { - this.NounPhrase = this.IsProperty(methodRef) - ? $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)" - : $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + if (this.IsProperty(methodRef)) + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + else if (methodRef.Name == ".ctor") + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + else + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; return InstructionHandleResult.NotCompatible; } } diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index 2f79809c..9ff43d45 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The IL instruction. public static MethodReference AsMethodReference(Instruction instruction) { - return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt + return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Newobj ? (MethodReference)instruction.Operand : null; } -- cgit From edb44cdb4011b794e45bc278c55e41af98ebf06b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 20:59:39 -0400 Subject: fix error reading empty translation files --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index bbf04556..0fd6801e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -53,6 +53,7 @@ * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). + * Fixed error if a mod translation file is empty. * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. * Fixed `Context.IsPlayerFree` being false during festivals. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c9266c69..24bf0d59 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -1143,8 +1143,17 @@ namespace StardewModdingAPI } // validate translations - foreach (string locale in translations.Keys) + foreach (string locale in translations.Keys.ToArray()) { + // skip empty files + if (translations[locale] == null || !translations[locale].Keys.Any()) + { + metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn); + translations.Remove(locale); + continue; + } + + // handle duplicates HashSet keys = new HashSet(StringComparer.InvariantCultureIgnoreCase); HashSet duplicateKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); foreach (string key in translations[locale].Keys.ToArray()) @@ -1155,7 +1164,6 @@ namespace StardewModdingAPI translations[locale].Remove(key); } } - if (duplicateKeys.Any()) metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); } -- cgit From 23392def0a1cfa7e0bf3df42c37a4d8f20aa9889 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 21:00:15 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 83c1b32d..efa834ef 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.19.3", + "Version": "2.6.0-beta.20", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 6bc8cf8b..26e6b41c 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.19.3", + "Version": "2.6.0-beta.20", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 3e0f51e7..dd46a3ca 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.19.3"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.20"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.23"); -- cgit From 4f854aea1530177f959fc01b1731ae4759830321 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 22:50:35 -0400 Subject: fix various build issues - installer not waiting until SaveBackup mod is compiled before preparing release build; - missing XML doc files for new toolkit assemblies; - missing XML doc file in SMAPI release build; - SaveBackup including toolkit DLL. --- build/prepare-install-package.targets | 4 ++++ src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj | 1 + src/SMAPI.sln | 1 + src/SMAPI/StardewModdingAPI.csproj | 2 +- .../StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 6 ++---- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 4 +--- 6 files changed, 10 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 33a92f71..79185896 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,8 +41,10 @@ + + @@ -58,8 +60,10 @@ + + diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index afba15a1..0ccbcc6c 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -54,6 +54,7 @@ {d5cfd923-37f1-4bc3-9be8-e506e202ac28} StardewModdingAPI.Toolkit.CoreInterfaces + False diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 99e93d62..d870c30c 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" ProjectSection(ProjectDependencies) = postProject + {E272EB5D-8C57-417E-8E60-C1079D3F53C4} = {E272EB5D-8C57-417E-8E60-C1079D3F53C4} {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 27e98f3a..0d0a5fe9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -43,7 +43,7 @@ x86 false $(SolutionDir)\..\bin\Release\SMAPI - $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml + $(SolutionDir)\..\bin\Release\SMAPI\StardewModdingAPI.xml TRACE true true diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj index e003122e..525931e5 100644 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -2,12 +2,10 @@ net4.5;netstandard2.0 + StardewModdingAPI false - - - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces - StardewModdingAPI + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 904f6786..21c130b3 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -3,10 +3,8 @@ net4.5;netstandard2.0 false - - - ..\..\bin\$(Configuration)\SMAPI.Toolkit + ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml -- cgit From 357b392ca239d1cc9000dbf9283f431a32d4c36d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 23:03:22 -0400 Subject: fix installer removing SaveBackup's config.json and previous backups --- src/SMAPI.Installer/InteractiveInstaller.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 1221f659..a5d5055f 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -21,6 +21,12 @@ namespace StardewModdingApi.Installer /// The name of the installer file in the package. private readonly string InstallerFileName = "install.exe"; + /// Mod files which shouldn't be deleted when deploying bundled mods (mod folder name => file names). + private readonly IDictionary> ProtectBundledFiles = new Dictionary>(StringComparer.InvariantCultureIgnoreCase) + { + ["SaveBackup"] = new HashSet(new[] { "backups", "config.json" }, StringComparer.InvariantCultureIgnoreCase) + }; + /// The value that represents Windows 7. private readonly Version Windows7Version = new Version(6, 1); @@ -386,10 +392,19 @@ namespace StardewModdingApi.Installer { this.PrintDebug($" adding {sourceDir.Name}..."); - // initialise target dir + // init/clear target dir DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name)); - this.InteractivelyDelete(targetDir.FullName); - targetDir.Create(); + if (targetDir.Exists) + { + this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet protectedFiles); + foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos()) + { + if (protectedFiles == null || !protectedFiles.Contains(entry.Name)) + this.InteractivelyDelete(entry.FullName); + } + } + else + targetDir.Create(); // copy files foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) -- cgit From ea6a82f9ee453f6f41f41f93fb452745670c2d20 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 23:14:47 -0400 Subject: add new files to uninstaller --- src/SMAPI.Installer/InteractiveInstaller.cs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index a5d5055f..f39486e1 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -102,8 +102,10 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.metadata.json"); yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); -- cgit From 1b1e89ebc5efc40c07d7a69daa455851f98baa96 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 23:14:54 -0400 Subject: bump versions for release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index efa834ef..a186a673 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.20", + "Version": "2.6.0-beta.20.1", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 26e6b41c..2edc359b 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.20", + "Version": "2.6.0-beta.20.1", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index dd46a3ca..5f1d61e9 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.20"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.20.1"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.23"); -- cgit From 3633fa4b0946574bdc3387dd073459ac878b5e7f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Jul 2018 19:01:59 -0400 Subject: add more intuitive error when using SMAPI 2.6 with SDV 1.2 --- src/SMAPI/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 24bf0d59..4f20ce37 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -156,7 +156,10 @@ namespace StardewModdingAPI // validate game version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); + if (Constants.GameVersion.IsBetween("1.2.30", "1.2.33") && Constants.ApiVersion.IsBetween("2.6-beta", "2.6")) // TODO: remove once SMAPI 2.6 is out of beta + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}. Make sure you install SMAPI 2.5.5 instead (SMAPI 2.6 beta is only for Stardew Valley 1.3 beta).", LogLevel.Error); + else + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } -- cgit From a03a94f6f273ca71dd0a8d38bbff4714cda8d309 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Jul 2018 19:25:23 -0400 Subject: fixed some game paths not detected by NuGet package --- src/SMAPI.ModBuildConfig/build/smapi.targets | 8 ++++++++ src/SMAPI.ModBuildConfig/package.nuspec | 1 + 2 files changed, 9 insertions(+) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 8ca178cc..d1c8a4eb 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -31,6 +31,7 @@ $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.steam/steam/steamapps/common/Stardew Valley $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley @@ -40,11 +41,18 @@ + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32)) + $(_SteamLibraryPath)\steamapps\common\Stardew Valley diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index bb142c33..b047bfa1 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -18,6 +18,7 @@ - Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3. - Added option to ignore files by regex pattern. - Added reference to new SMAPI DLL. + - Fixed some game paths not detected by NuGet package.
    -- cgit From 6bd2c55a76cdd09f327145e9368f9e831ddcbfff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Jul 2018 19:36:30 -0400 Subject: fix XACT references failing new compatibility checks on Linux/Mac --- src/SMAPI/Constants.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 5f1d61e9..8c11c24a 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -118,7 +118,8 @@ namespace StardewModdingAPI "Stardew Valley", "Microsoft.Xna.Framework", "Microsoft.Xna.Framework.Game", - "Microsoft.Xna.Framework.Graphics" + "Microsoft.Xna.Framework.Graphics", + "Microsoft.Xna.Framework.Xact" }; targetAssemblies = new[] { -- cgit From 76e1fd3905fc16a99225ef212a21e9365adeab88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Jul 2018 20:19:02 -0400 Subject: fix console commands being invoked asynchronously (#562) --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 28 ++++++++++++++++++++++++++-- src/SMAPI/Program.cs | 29 +++++++---------------------- 3 files changed, 34 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0fd6801e..39b08b5a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -58,6 +58,7 @@ * Fixed console command input not saved to the log. * Fixed `Context.IsPlayerFree` being false during festivals. * Fixed `helper.ModRegistry.GetApi` errors not always mentioning which interface caused the issue. + * Fixed console commands being invoked asynchronously. * Fixed mods able to intercept other mods' assets via the internal asset keys. * Fixed mods able to indirectly change other mods' data through shared content caches. * Fixed `SemanticVersion` allowing invalid versions in some cases. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 777bc478..a685dfce 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -95,7 +96,7 @@ namespace StardewModdingAPI.Framework private bool IsInitialised; /// The number of update ticks which have already executed. - private uint TicksElapsed = 0; + private uint TicksElapsed; /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; @@ -107,6 +108,9 @@ namespace StardewModdingAPI.Framework /// SMAPI's content manager. public ContentCoordinator ContentCore { get; private set; } + /// Manages console commands. + public CommandManager CommandManager { get; } = new CommandManager(); + /// Manages input visible to the game. public SInputState Input => (SInputState)Game1.input; @@ -116,6 +120,10 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } + /// A list of queued commands to execute. + /// This property must be threadsafe, since it's accessed from a separate console input thread. + public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); + /********* ** Protected methods @@ -229,7 +237,7 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log("Game loader synchronising...", LogLevel.Trace); while (Game1.currentLoader?.MoveNext() == true) - continue; + ; Game1.currentLoader = null; this.Monitor.Log("Game loader done.", LogLevel.Trace); } @@ -256,6 +264,22 @@ namespace StardewModdingAPI.Framework return; } + /********* + ** Execute commands + *********/ + while (this.CommandQueue.TryDequeue(out string rawInput)) + { + try + { + if (!this.CommandManager.Trigger(rawInput)) + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + } + catch (Exception ex) + { + this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + /********* ** Update input *********/ diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 4f20ce37..a9546233 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -80,10 +80,6 @@ namespace StardewModdingAPI /// This is initialised after the game starts. private DeprecationManager DeprecationManager; - /// Manages console commands. - /// This is initialised after the game starts. - private CommandManager CommandManager; - /// Manages SMAPI events for mods. private readonly EventManager EventManager; @@ -394,7 +390,6 @@ namespace StardewModdingAPI // load core components this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); - this.CommandManager = new CommandManager(); // redirect direct console output { @@ -485,8 +480,8 @@ namespace StardewModdingAPI { // prepare console this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); - this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + this.GameInstance.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); + this.GameInstance.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); // start handling command line input Thread inputThread = new Thread(() => @@ -498,19 +493,9 @@ namespace StardewModdingAPI if (string.IsNullOrWhiteSpace(input)) continue; - // write input to log file + // handle command this.Monitor.LogUserInput(input); - - // parse input - try - { - if (!this.CommandManager.Trigger(input)) - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); - } - catch (Exception ex) - { - this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); - } + this.GameInstance.CommandQueue.Enqueue(input); } }); inputThread.Start(); @@ -867,7 +852,7 @@ namespace StardewModdingAPI IModHelper modHelper; { IModEvents events = new ModEvents(metadata, this.EventManager); - ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); + ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.GameInstance.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); @@ -1187,7 +1172,7 @@ namespace StardewModdingAPI case "help": if (arguments.Any()) { - Command result = this.CommandManager.Get(arguments[0]); + Command result = this.GameInstance.CommandManager.Get(arguments[0]); if (result == null) this.Monitor.Log("There's no command with that name.", LogLevel.Error); else @@ -1196,7 +1181,7 @@ namespace StardewModdingAPI else { string message = "The following commands are registered:\n"; - IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); + IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); foreach (var group in groups) { string modName = group.Key; -- cgit From 76f12a9aa9ba504d62c4511d67b756dff40b0efd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Jul 2018 21:28:34 -0400 Subject: bump version for beta release --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 8c11c24a..93bfc4c3 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.20.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.20.2"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.23"); -- cgit From 5050bd75e7b86749d994ae268cdc3dabb7f7fcf8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 15 Jul 2018 20:59:31 -0400 Subject: fix misnamed types --- src/SMAPI/Events/IInputEvents.cs | 6 +++--- src/SMAPI/Events/InputButtonPressedEventArgs.cs | 4 ++-- src/SMAPI/Events/InputButtonReleasedEventArgs.cs | 4 ++-- src/SMAPI/Events/InputCursorMovedEventArgs.cs | 4 ++-- src/SMAPI/Framework/Events/EventManager.cs | 12 ++++++------ src/SMAPI/Framework/Events/ModInputEvents.cs | 6 +++--- src/SMAPI/Framework/SGame.cs | 6 +++--- 7 files changed, 21 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 64d82c57..8e2ef406 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -6,13 +6,13 @@ namespace StardewModdingAPI.Events public interface IInputEvents { /// Raised after the player presses a button on the keyboard, controller, or mouse. - event EventHandler ButtonPressed; + event EventHandler ButtonPressed; /// Raised after the player releases a button on the keyboard, controller, or mouse. - event EventHandler ButtonReleased; + event EventHandler ButtonReleased; /// Raised after the player moves the in-game cursor. - event EventHandler CursorMoved; + event EventHandler CursorMoved; /// Raised after the player scrolls the mouse wheel. event EventHandler MouseWheelScrolled; diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs index 002f7cf1..8c6844dd 100644 --- a/src/SMAPI/Events/InputButtonPressedEventArgs.cs +++ b/src/SMAPI/Events/InputButtonPressedEventArgs.cs @@ -4,7 +4,7 @@ using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Events { /// Event arguments when a button is pressed. - public class InputButtonPressedArgsInput : EventArgs + public class InputButtonPressedEventArgs : EventArgs { /********* ** Properties @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Events /// The button on the controller, keyboard, or mouse. /// The cursor position. /// The game's current input state. - internal InputButtonPressedArgsInput(SButton button, ICursorPosition cursor, SInputState inputState) + internal InputButtonPressedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) { this.Button = button; this.Cursor = cursor; diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs index bc5e4a89..4b0bc326 100644 --- a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs +++ b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs @@ -4,7 +4,7 @@ using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Events { /// Event arguments when a button is released. - public class InputButtonReleasedArgsInput : EventArgs + public class InputButtonReleasedEventArgs : EventArgs { /********* ** Properties @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Events /// The button on the controller, keyboard, or mouse. /// The cursor position. /// The game's current input state. - internal InputButtonReleasedArgsInput(SButton button, ICursorPosition cursor, SInputState inputState) + internal InputButtonReleasedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) { this.Button = button; this.Cursor = cursor; diff --git a/src/SMAPI/Events/InputCursorMovedEventArgs.cs b/src/SMAPI/Events/InputCursorMovedEventArgs.cs index 02e1ee2c..53aac5b3 100644 --- a/src/SMAPI/Events/InputCursorMovedEventArgs.cs +++ b/src/SMAPI/Events/InputCursorMovedEventArgs.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI.Events { /// Event arguments when the in-game cursor is moved. - public class InputCursorMovedArgsInput : EventArgs + public class InputCursorMovedEventArgs : EventArgs { /********* ** Accessors @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The previous cursor position. /// The new cursor position. - public InputCursorMovedArgsInput(ICursorPosition oldPosition, ICursorPosition newPosition) + public InputCursorMovedEventArgs(ICursorPosition oldPosition, ICursorPosition newPosition) { this.OldPosition = oldPosition; this.NewPosition = newPosition; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 4de333a3..168ddde0 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -27,13 +27,13 @@ namespace StardewModdingAPI.Framework.Events ** Input ****/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonPressed; + public readonly ManagedEvent Input_ButtonPressed; /// Raised after the player released a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Input_ButtonReleased; + public readonly ManagedEvent Input_ButtonReleased; /// Raised after the player moves the in-game cursor. - public readonly ManagedEvent Input_CursorMoved; + public readonly ManagedEvent Input_CursorMoved; /// Raised after the player scrolls the mouse wheel. public readonly ManagedEvent Input_MouseWheelScrolled; @@ -268,9 +268,9 @@ namespace StardewModdingAPI.Framework.Events this.GameLoop_Updating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updating)); this.GameLoop_Updated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Updated)); - this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); - this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.Input_ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.Input_CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.Input_MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.World_BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 387ea87a..feca34f3 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -10,21 +10,21 @@ namespace StardewModdingAPI.Framework.Events ** Accessors *********/ /// Raised after the player presses a button on the keyboard, controller, or mouse. - public event EventHandler ButtonPressed + public event EventHandler ButtonPressed { add => this.EventManager.Input_ButtonPressed.Add(value); remove => this.EventManager.Input_ButtonPressed.Remove(value); } /// Raised after the player releases a button on the keyboard, controller, or mouse. - public event EventHandler ButtonReleased + public event EventHandler ButtonReleased { add => this.EventManager.Input_ButtonReleased.Add(value); remove => this.EventManager.Input_ButtonReleased.Remove(value); } /// Raised after the player moves the in-game cursor. - public event EventHandler CursorMoved + public event EventHandler CursorMoved { add => this.EventManager.Input_CursorMoved.Add(value); remove => this.EventManager.Input_CursorMoved.Remove(value); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a685dfce..961fae08 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -430,7 +430,7 @@ namespace StardewModdingAPI.Framework ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; this.Watchers.CursorWatcher.Reset(); - this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(was, now)); + this.Events.Input_CursorMoved.Raise(new InputCursorMovedEventArgs(was, now)); } // raise mouse wheel scrolled @@ -456,7 +456,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); - this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState)); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedEventArgs(button, cursor, inputState)); this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events @@ -478,7 +478,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); - this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState)); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedEventArgs(button, cursor, inputState)); this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events -- cgit From cfe608d0d02b91533eab160c512bf4b3f4867a6c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 16 Jul 2018 20:04:50 -0400 Subject: update compatibility list --- src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json index d9b2954c..e72efb39 100644 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -505,7 +505,7 @@ "Data Maps": { "ID": "Pathoschild.DataMaps", "Default | UpdateKey": "Nexus:1691", - "~1.3 | Status": "AssumeBroken" // broke in SDV 1.3 + "~1.4 | Status": "AssumeBroken" // replaced by Data Layers }, "Debug Mode": { @@ -654,6 +654,11 @@ "Default | UpdateKey": "Nexus:1941" }, + "Fix Scythe Exp": { + "ID": "bcmpinc.FixScytheExp", + "~0.2 | Status": "AssumeBroken" // Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator. + }, + "Flower Color Picker": { "ID": "spacechase0.FlowerColorPicker", "Default | UpdateKey": "Nexus:1229" -- cgit From 90c52c73afcfdc7fff63aa74e23fc10b3b379c5d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 16 Jul 2018 20:22:19 -0400 Subject: bump versions for beta release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index a186a673..7ff11d52 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.20.1", + "Version": "2.6.0-beta.21", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 2edc359b..2e62b41e 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.20.1", + "Version": "2.6.0-beta.21", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 93bfc4c3..c613d8b0 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,10 +29,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.20.2"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.21"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.23"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.25"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 37f867683b55b863893f7f8529248a772b137a2e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 17 Jul 2018 00:28:30 -0400 Subject: fix XML docs --- src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs index 6c07d374..7375f005 100644 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI /// The unique mod ID. string UniqueID { get; } - /// The name of the DLL in the directory that has the method. Mutually exclusive with . + /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . string EntryDll { get; } /// The mod which will read this as a content pack. Mutually exclusive with . -- cgit From 0806a9724214f96eedf741ca6b5cc2aab0dbd14b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Jul 2018 19:18:44 -0400 Subject: bump NuGet package version for release --- src/SMAPI.ModBuildConfig/package.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index b047bfa1..3d6f2598 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.1.0-beta-20180630 + 2.1.0 Build package for SMAPI mods Pathoschild Pathoschild @@ -10,7 +10,7 @@ https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png - Automates the build configuration for crossplatform Stardew Valley SMAPI mods. + Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later. 2.1: - Added support for Stardew Valley 1.3. -- cgit From 7ba3f9bade1dd67221d21f1d9be7375cd2ee26c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Jul 2018 20:36:57 -0400 Subject: remove now-unneeded save guard (#568) We previously checked !SaveGame.IsProcessing to avoid running events while the game was saving. Due to a bug in Stardew Valley on Linux/Mac, this flag is never unset when the save completes. It's no longer needed anyway, since SMAPI now runs the save synchronously. --- src/SMAPI/Framework/SGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 961fae08..05fedc3d 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -344,7 +344,7 @@ namespace StardewModdingAPI.Framework bool wasWorldReady = Context.IsWorldReady; if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle) this.MarkWorldNotReady(); - else if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) + else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) { if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) this.AfterLoadTimer.Decrement(); -- cgit From 670ff77363bae56c7514b83e7c126cacd318f440 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jul 2018 12:31:29 -0400 Subject: remove 'use SMAPI 2.5.5' message when running Stardew Valley 1.2 (#569) --- src/SMAPI/Program.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a9546233..92e7ca17 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -152,10 +152,7 @@ namespace StardewModdingAPI // validate game version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - if (Constants.GameVersion.IsBetween("1.2.30", "1.2.33") && Constants.ApiVersion.IsBetween("2.6-beta", "2.6")) // TODO: remove once SMAPI 2.6 is out of beta - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}. Make sure you install SMAPI 2.5.5 instead (SMAPI 2.6 beta is only for Stardew Valley 1.3 beta).", LogLevel.Error); - else - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } -- cgit From 84d52b1735204550c6369270c03f61944c2c88bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jul 2018 12:43:04 -0400 Subject: make beta version on smapi.io optional (#569) --- src/SMAPI.Web/Controllers/IndexController.cs | 11 +++++++++-- src/SMAPI.Web/Controllers/LogParserController.cs | 10 +++++----- src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs | 15 --------------- src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs | 18 ++++++++++++++++++ src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 6 +++--- src/SMAPI.Web/appsettings.Development.json | 7 +++++-- src/SMAPI.Web/appsettings.json | 9 ++++++--- 8 files changed, 47 insertions(+), 31 deletions(-) delete mode 100644 src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 09bad112..8c4a0332 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -6,8 +6,10 @@ using System.Threading.Tasks; using HtmlAgilityPack; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Clients.GitHub; +using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers @@ -20,6 +22,9 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Properties *********/ + /// The site config settings. + private readonly SiteConfig SiteConfig; + /// The cache in which to store release data. private readonly IMemoryCache Cache; @@ -39,10 +44,12 @@ namespace StardewModdingAPI.Web.Controllers /// Construct an instance. /// The cache in which to store release data. /// The GitHub API client. - public IndexController(IMemoryCache cache, IGitHubClient github) + /// The context config settings. + public IndexController(IMemoryCache cache, IGitHubClient github, IOptions siteConfig) { this.Cache = cache; this.GitHub = github; + this.SiteConfig = siteConfig.Value; } /// Display the index page. @@ -60,7 +67,7 @@ namespace StardewModdingAPI.Web.Controllers IndexVersionModel stableVersionModel = stableVersion != null ? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl) : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong) - IndexVersionModel betaVersionModel = betaVersion != null + IndexVersionModel betaVersionModel = betaVersion != null && this.SiteConfig.EnableSmapiBeta ? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl) : null; diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 354bdb06..17f8d3aa 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -21,8 +21,8 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Properties *********/ - /// The log parser config settings. - private readonly ContextConfig Config; + /// The site config settings. + private readonly SiteConfig Config; /// The underlying Pastebin client. private readonly IPastebinClient Pastebin; @@ -39,11 +39,11 @@ namespace StardewModdingAPI.Web.Controllers ** Constructor ***/ /// Construct an instance. - /// The context config settings. + /// The context config settings. /// The Pastebin API client. - public LogParserController(IOptions contextProvider, IPastebinClient pastebin) + public LogParserController(IOptions siteConfig, IPastebinClient pastebin) { - this.Config = contextProvider.Value; + this.Config = siteConfig.Value; this.Pastebin = pastebin; } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs deleted file mode 100644 index 117462f4..00000000 --- a/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ConfigModels -{ - /// The config settings for the app context. - public class ContextConfig // must be public to pass into views - { - /********* - ** Accessors - *********/ - /// The root URL for the app. - public string RootUrl { get; set; } - - /// The root URL for the log parser. - public string LogParserUrl { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs new file mode 100644 index 00000000..3d428015 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The site config settings. + public class SiteConfig // must be public to pass into views + { + /********* + ** Accessors + *********/ + /// The root URL for the app. + public string RootUrl { get; set; } + + /// The root URL for the log parser. + public string LogParserUrl { get; set; } + + /// Whether to show SMAPI beta versions on the main page, if any. + public bool EnableSmapiBeta { get; set; } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index ced6e1c7..bf3ec9a1 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Web // init configuration services .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(this.Configuration.GetSection("Context")) + .Configure(this.Configuration.GetSection("Site")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index d435e760..29da9100 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -1,6 +1,6 @@ @using Microsoft.Extensions.Options @using StardewModdingAPI.Web.Framework.ConfigModels -@inject IOptions ContextConfig +@inject IOptions SiteConfig @@ -15,8 +15,8 @@ diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 495af120..67bb7748 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -16,10 +16,13 @@ "Microsoft": "Information" } }, - "Context": { + + "Site": { "RootUrl": "http://localhost:59482/", - "LogParserUrl": "http://localhost:59482/log/" + "LogParserUrl": "http://localhost:59482/log/", + "EnableSmapiBeta": false }, + "ApiClients": { "GitHubUsername": null, "GitHubPassword": null, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 837ba536..9e3270ae 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -13,10 +13,13 @@ "Default": "Warning" } }, - "Context": { - "RootUrl": null, // see top note - "LogParserUrl": null // see top note + + "Site": { + "RootUrl": null, // see top note + "LogParserUrl": null, // see top note + "EnableSmapiBeta": null // see top note }, + "ApiClients": { "UserAgent": "SMAPI/{0} (+https://smapi.io)", -- cgit From b47068248af02a9815030cd7bd5f0bd1c49cc235 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 Jul 2018 19:29:01 -0400 Subject: tweak 'share your log' message --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 92e7ca17..6012b15a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -274,7 +274,7 @@ namespace StardewModdingAPI 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: http://community.playstarbound.com/threads/108375/.", LogLevel.Error); - this.Monitor.Log($"If you ask for help, make sure to attach this file: {Constants.FatalCrashLog}", LogLevel.Error); + this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error); this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); Console.ReadKey(); File.Delete(Constants.FatalCrashLog); -- cgit From 43b9113f9d06bc4feda7a89365da028122d5aeee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 Jul 2018 19:48:14 -0400 Subject: bump versions for upcoming release --- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 2 +- src/SMAPI/Constants.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 7ff11d52..f89049c6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.6.0-beta.21", + "Version": "2.6.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 2e62b41e..ee0f2abb 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,7 +1,7 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "2.6.0-beta.21", + "Version": "2.6.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index c613d8b0..a6cddbe4 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,10 +29,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.21"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6.0"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.25"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.27"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 52cf953f685c65b2b6814e375ec9a5ffa03c440a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Aug 2018 06:01:53 -0400 Subject: mention SMAPI-crash.txt in log parser instructions --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index f5501fed..e735e8f3 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -77,7 +77,7 @@ else if (Model.ParsedLog?.IsValid == true)
  • Click the options menu (might be labeled Go or ).
  • Choose Enter Location.
  • Enter this exact text:
    ~/.config/StardewValley/ErrorLogs
  • -
  • The log file is SMAPI-latest.txt.
  • +
  • The log file is SMAPI-crash.txt if it exists, otherwise SMAPI-latest.txt.
  • @@ -86,7 +86,7 @@ else if (Model.ParsedLog?.IsValid == true)
  • Open the Finder app.
  • Click Go at the top, then Enter Location.
  • Enter this exact text:
    ~/.config/StardewValley/ErrorLogs
  • -
  • The log file is SMAPI-latest.txt.
  • +
  • The log file is SMAPI-crash.txt if it exists, otherwise SMAPI-latest.txt.
  • @@ -94,7 +94,7 @@ else if (Model.ParsedLog?.IsValid == true)
    1. Press the Windows and R buttons at the same time.
    2. In the 'run' box that appears, enter this exact text:
      %appdata%\StardewValley\ErrorLogs
    3. -
    4. The log file is SMAPI-latest.txt.
    5. +
    6. The log file is SMAPI-crash.txt if it exists, otherwise SMAPI-latest.txt.
    -- cgit