From 2ff937397163f0ad5940b636bc7312ac747d9c39 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 10:59:57 -0400 Subject: [PATCH 01/59] fix compatibility check crashing for players with SDV 1.08 --- docs/release-notes.md | 6 +++++- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 3 ++- src/SMAPI/Framework/GameVersion.cs | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 99e771ce..fc56adc8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,8 @@ # Release notes +## 2.1 (upcoming) +* For players: + * Fixed compatibility check crashing for players with Stardew Valley 1.08. + ## 2.0 ### Release highlights * **Mod update checks** @@ -18,7 +22,7 @@ SMAPI 2.0 adds several features to enable new kinds of mods (see [API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)). - The **content API** lets you edit, inject, and reload XNB data loaded by the game at any time. This let SMAPI mods do + 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. seasonal textures, NPC clothing that depend on the weather or location, etc). diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 03cd26c9..73ecd56e 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using NUnit.Framework; @@ -239,6 +239,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("1.06")] [TestCase("1.07")] [TestCase("1.07a")] + [TestCase("1.08")] [TestCase("1.1")] [TestCase("1.11")] [TestCase("1.2")] diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 48159f61..1884afe9 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace StardewModdingAPI.Framework @@ -22,6 +22,7 @@ namespace StardewModdingAPI.Framework ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", ["1.07a"] = "1.0.8-prerelease1", + ["1.08"] = "1.0.8", ["1.11"] = "1.1.1" }; From 51a2c3991f3c76197afb21a42a30f2a91a7f9908 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 16:47:32 -0400 Subject: [PATCH 02/59] simplify SelectiveStringEnumConverter implementation --- .../Framework/Serialisation/JsonHelper.cs | 7 +++--- .../SelectiveStringEnumConverter.cs | 25 ++++--------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 3193aa3c..77b93b66 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; -using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.Serialisation { @@ -20,7 +19,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) + new SelectiveStringEnumConverter(), + new SelectiveStringEnumConverter(), + new SelectiveStringEnumConverter() } }; diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs index 37108556..e825c880 100644 --- a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs @@ -1,37 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System; using Newtonsoft.Json.Converters; namespace StardewModdingAPI.Framework.Serialisation { - /// A variant of which only converts certain enums. - internal class SelectiveStringEnumConverter : StringEnumConverter + /// A variant of which only converts a specified enum. + /// The enum type. + internal class SelectiveStringEnumConverter : StringEnumConverter { - /********* - ** Properties - *********/ - /// The enum type names to convert. - private readonly HashSet Types; - - /********* ** Public methods *********/ - /// Construct an instance. - /// The enum types to convert. - public SelectiveStringEnumConverter(params Type[] types) - { - this.Types = new HashSet(types.Select(p => p.FullName)); - } - /// Get whether this instance can convert the specified object type. /// The object type. public override bool CanConvert(Type type) { return base.CanConvert(type) - && this.Types.Contains((Nullable.GetUnderlyingType(type) ?? type).FullName); + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); } } } From a4fb2331fe57102aa8e8b30efb8095a1edb6b923 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 16:58:42 -0400 Subject: [PATCH 03/59] simplify JSON converter name --- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 6 +++--- ...lectiveStringEnumConverter.cs => StringEnumConverter.cs} | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/SMAPI/Framework/Serialisation/{SelectiveStringEnumConverter.cs => StringEnumConverter.cs} (90%) diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 77b93b66..d923ec0c 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -19,9 +19,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(), - new SelectiveStringEnumConverter(), - new SelectiveStringEnumConverter() + new StringEnumConverter(), + new StringEnumConverter(), + new StringEnumConverter() } }; diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs similarity index 90% rename from src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs rename to src/SMAPI/Framework/Serialisation/StringEnumConverter.cs index e825c880..7afe86cd 100644 --- a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ b/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Framework.Serialisation { /// A variant of which only converts a specified enum. /// The enum type. - internal class SelectiveStringEnumConverter : StringEnumConverter + internal class StringEnumConverter : StringEnumConverter { /********* ** Public methods diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index b8d5990e..6f7c2b3f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -173,7 +173,7 @@ - + From 36b4e550f1945ef710fca2c6deab7df94e708ef7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Oct 2017 21:26:00 -0400 Subject: [PATCH 04/59] fix e.SuppressButton() in input events not suppressing keyboard buttons --- docs/release-notes.md | 3 +++ src/SMAPI/Events/EventArgsInput.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index fc56adc8..0471874c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,9 @@ * For players: * Fixed compatibility check crashing for players with Stardew Valley 1.08. +* For modders: + * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. + ## 2.0 ### Release highlights * **Mod update checks** diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 66cb19f2..617dac35 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Events { // keyboard if (this.Button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Except(new[] { key }).ToArray()); + Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); // controller else if (this.Button.TryGetController(out Buttons controllerButton)) From 53df85f3123f8d9cb00013bb32b61c220ccad697 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 20 Oct 2017 16:37:22 -0400 Subject: [PATCH 05/59] enable access to public members using reflection API --- docs/release-notes.md | 1 + src/SMAPI/Framework/Reflection/Reflector.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0471874c..285d9df3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ * Fixed compatibility check crashing for players with Stardew Valley 1.08. * For modders: + * The reflection API now works with public code to simplify mod integrations. * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. ## 2.0 diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 5c2d90fa..23a48505 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && field == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); return field; @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateField GetPrivateField(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); if (required && field == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); return field; @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && property == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); return property; @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && property == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); return property; @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); return method; @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); return method; @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); return method; @@ -155,7 +155,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); return method; From 85a8959e97e90b30ac8291904838e18f102e97c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Oct 2017 21:51:48 -0400 Subject: [PATCH 06/59] fix mods which implement IAssetLoader being marked as conflicting with themselves --- docs/release-notes.md | 1 + src/SMAPI/Framework/SContentManager.cs | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 285d9df3..e4b2bccd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For modders: * The reflection API now works with public code to simplify mod integrations. * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. + * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. ## 2.0 ### Release highlights diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index db202567..2f5d104f 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -510,16 +510,12 @@ namespace StardewModdingAPI.Framework { foreach (var entry in entries) { - IModMetadata metadata = entry.Key; + IModMetadata mod = entry.Key; IList interceptors = entry.Value; - // special case if mod is an interceptor - if (metadata.Mod is T modAsInterceptor) - yield return new KeyValuePair(metadata, modAsInterceptor); - // registered editors foreach (T interceptor in interceptors) - yield return new KeyValuePair(metadata, interceptor); + yield return new KeyValuePair(mod, interceptor); } } From f74321addc79a5616cc0f43e4f5f4b8154fac827 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 13:13:14 -0400 Subject: [PATCH 07/59] fix SMAPI blocking reflection access to vanilla members on overridden types (#371) --- docs/release-notes.md | 1 + .../Framework/ModHelpers/ReflectionHelper.cs | 99 ++++++++++++------- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index e4b2bccd..199e32c5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * The reflection API now works with public code to simplify mod integrations. * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. + * Fixed SMAPI blocking reflection access to vanilla members on overridden types. ## 2.0 ### Release highlights diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 8d435416..8788b142 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -42,8 +43,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Returns the field wrapper, or null if the field doesn't exist and is false. public IPrivateField GetPrivateField(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateField(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateField(obj, name, required) + ); } /// Get a private static field. @@ -53,8 +55,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateField GetPrivateField(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateField(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateField(type, name, required) + ); } /**** @@ -67,8 +70,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateProperty(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateProperty(obj, name, required) + ); } /// Get a private static property. @@ -78,8 +82,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateProperty(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateProperty(type, name, required) + ); } /**** @@ -98,7 +103,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); IPrivateField field = this.GetPrivateField(obj, name, required); return field != null ? field.GetValue() @@ -117,7 +121,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); IPrivateField field = this.GetPrivateField(type, name, required); return field != null ? field.GetValue() @@ -133,8 +136,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(obj, name, required) + ); } /// Get a private static method. @@ -143,8 +147,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(type, name, required) + ); } /**** @@ -157,8 +162,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required) + ); } /// Get a private static method. @@ -168,33 +174,60 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(type, name, argumentTypes, required) + ); } /********* ** Private methods *********/ - /// Assert that mods can use the reflection helper to access the given type. - /// The type being accessed. - private void AssertAccessAllowed(Type type) + /// Assert that mods can use the reflection helper to access the given member. + /// The field value type. + /// The field being accessed. + /// Returns the same field instance for convenience. + private IPrivateField AssertAccessAllowed(IPrivateField field) { - // validate type namespace - if (type.Namespace != null) - { - string rootSmapiNamespace = typeof(Program).Namespace; - if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) - throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); - } + this.AssertAccessAllowed(field?.FieldInfo); + return field; } - /// Assert that mods can use the reflection helper to access the given type. - /// The object being accessed. - private void AssertAccessAllowed(object obj) + /// Assert that mods can use the reflection helper to access the given member. + /// The property value type. + /// The property being accessed. + /// Returns the same property instance for convenience. + private IPrivateProperty AssertAccessAllowed(IPrivateProperty property) { - if (obj != null) - this.AssertAccessAllowed(obj.GetType()); + this.AssertAccessAllowed(property?.PropertyInfo); + return property; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The method being accessed. + /// Returns the same method instance for convenience. + private IPrivateMethod AssertAccessAllowed(IPrivateMethod method) + { + this.AssertAccessAllowed(method?.MethodInfo); + return method; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The member being accessed. + private void AssertAccessAllowed(MemberInfo member) + { + if (member == null) + return; + + // get type which defines the member + Type declaringType = member.DeclaringType; + if (declaringType == null) + throw new InvalidOperationException($"Can't validate access to {member.MemberType} {member.Name} because it has no declaring type."); // should never happen + + // validate access + string rootNamespace = typeof(Program).Namespace; + if (declaringType.Namespace == rootNamespace || declaringType.Namespace?.StartsWith(rootNamespace + ".") == true) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning. (Detected access to {declaringType.FullName}.{member.Name}.)"); } } } From 99c8dd79406f5099194d72e26085a49939705259 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 15:07:06 -0400 Subject: [PATCH 08/59] add InputButton.ToSButton() extension --- docs/release-notes.md | 6 ++++-- src/SMAPI/SButton.cs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 199e32c5..65536915 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,8 +4,10 @@ * Fixed compatibility check crashing for players with Stardew Valley 1.08. * For modders: - * The reflection API now works with public code to simplify mod integrations. - * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. + * Added support for public code in reflection API, to simplify mod integrations. + * Improved input events: + * Added `ToSButton()` extension for the game's `Game1.options` button type. + * Fixed `e.SuppressButton()` not correctly suppressing keyboard buttons. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. * Fixed SMAPI blocking reflection access to vanilla members on overridden types. diff --git a/src/SMAPI/SButton.cs b/src/SMAPI/SButton.cs index 0ec799db..bd6635c7 100644 --- a/src/SMAPI/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -615,6 +615,18 @@ namespace StardewModdingAPI return (SButton)(SButtonExtensions.ControllerOffset + key); } + /// Get the equivalent for the given button. + /// The Stardew Valley button to convert. + internal static SButton ToSButton(this InputButton input) + { + // derived from InputButton constructors + if (input.mouseLeft) + return SButton.MouseLeft; + if (input.mouseRight) + return SButton.MouseRight; + return input.key.ToSButton(); + } + /// Get the equivalent for the given button. /// The button to convert. /// The keyboard equivalent. From ed56cb714d7fb76f3c1b9d2f2e7b7627f8accc70 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 15:09:36 -0400 Subject: [PATCH 09/59] replace input events' e.IsClick with better-designed e.IsActionButton and e.IsUseToolButton --- docs/release-notes.md | 3 +++ src/SMAPI/Events/EventArgsInput.cs | 18 +++++++++++++----- src/SMAPI/Events/InputEvents.cs | 15 ++++++++------- src/SMAPI/Framework/SGame.cs | 13 ++++++------- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 65536915..452fd40a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,8 +6,11 @@ * For modders: * Added support for public code in reflection API, to simplify mod integrations. * Improved input events: + * Added `e.IsActionButton` and `e.IsUseToolButton`. * Added `ToSButton()` extension for the game's `Game1.options` button type. + * Deprecated `e.IsClick`, which is limited and unclear. Use `IsActionButton` or `IsUseToolButton` instead. * Fixed `e.SuppressButton()` not correctly suppressing keyboard buttons. + * Fixed `e.IsClick` (now `e.IsActionButton`) ignoring custom key bindings. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. * Fixed SMAPI blocking reflection access to vanilla members on overridden types. diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 617dac35..ff904675 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Events @@ -20,7 +19,14 @@ namespace StardewModdingAPI.Events public ICursorPosition Cursor { get; set; } /// Whether the input is considered a 'click' by the game for enabling action. - public bool IsClick { get; } + [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 + public bool IsClick => this.IsActionButton; + + /// Whether the input should trigger actions on the affected tile. + public bool IsActionButton { get; } + + /// Whether the input should use tools on the affected tile. + public bool IsUseToolButton { get; } /********* @@ -29,12 +35,14 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) + /// 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) { this.Button = button; this.Cursor = cursor; - this.IsClick = isClick; + this.IsActionButton = isActionButton; + this.IsUseToolButton = isUseToolButton; } /// Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event. diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index c31eb698..985aed99 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -1,6 +1,5 @@ using System; using StardewModdingAPI.Framework; -using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Events { @@ -24,20 +23,22 @@ namespace StardewModdingAPI.Events /// Encapsulates monitoring and logging. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); } /// Raise a event. /// Encapsulates monitoring and logging. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6f8f7cef..ca19d726 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -12,7 +12,6 @@ using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -371,7 +370,8 @@ namespace StardewModdingAPI.Framework SButton[] previousPressedKeys = this.PreviousPressedButtons; SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); - bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); + bool isUseToolButton = Game1.options.useToolButton.Any(p => framePressedKeys.Contains(p.ToSButton())); + bool isActionButton = !isUseToolButton && Game1.options.actionButton.Any(p => framePressedKeys.Contains(p.ToSButton())); // get cursor position ICursorPosition cursor; @@ -388,7 +388,7 @@ namespace StardewModdingAPI.Framework // raise button pressed foreach (SButton button in framePressedKeys) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isActionButton, isUseToolButton); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -408,10 +408,9 @@ namespace StardewModdingAPI.Framework // raise button released foreach (SButton button in frameReleasedKeys) { - bool wasClick = - (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click - || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); + bool wasUseToolButton = (from opt in Game1.options.useToolButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any(); + bool wasActionButton = !wasUseToolButton && (from opt in Game1.options.actionButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any(); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasActionButton, wasUseToolButton); // legacy events if (button.TryGetKeyboard(out Keys key)) From 8c97a63a82729efe56d73928e9afb436dbffea56 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 23 Oct 2017 03:24:53 -0400 Subject: [PATCH 10/59] improve content manager thread safety, create content cache wrapper (#373) --- src/SMAPI/Framework/Content/ContentCache.cs | 150 +++++++++++++ src/SMAPI/Framework/SContentManager.cs | 234 +++++++++++--------- src/SMAPI/Framework/SGame.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 284 insertions(+), 103 deletions(-) create mode 100644 src/SMAPI/Framework/Content/ContentCache.cs diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs new file mode 100644 index 00000000..10c41d08 --- /dev/null +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.Content +{ + /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localisation, loading content, etc. It assumes all keys passed in are already normalised. + internal class ContentCache + { + /********* + ** Properties + *********/ + /// The underlying asset cache. + private readonly IDictionary Cache; + + /// The possible directory separator characters in an asset key. + private readonly char[] PossiblePathSeparators; + + /// The preferred directory separator chaeacter in an asset key. + private readonly string PreferredPathSeparator; + + /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. + private readonly Func NormaliseAssetNameForPlatform; + + + /********* + ** Accessors + *********/ + /// Get or set the value of a raw cache entry. + /// The cache key. + public object this[string key] + { + get => this.Cache[key]; + set => this.Cache[key] = value; + } + + /// The current cache keys. + public IEnumerable Keys => this.Cache.Keys; + + + /********* + ** Public methods + *********/ + /**** + ** Constructor + ****/ + /// Construct an instance. + /// The underlying content manager whose cache to manage. + /// Simplifies access to private game code. + /// The possible directory separator characters in an asset key. + /// The preferred directory separator chaeacter in an asset key. + public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) + { + // init + this.Cache = reflection.GetPrivateField>(contentManager, "loadedAssets").GetValue(); + this.PossiblePathSeparators = possiblePathSeparators; + this.PreferredPathSeparator = preferredPathSeparator; + + // get key normalisation logic + if (Constants.TargetPlatform == Platform.Windows) + { + IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); + this.NormaliseAssetNameForPlatform = path => method.Invoke(path); + } + else + this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + } + + /**** + ** Fetch + ****/ + /// Get whether the cache contains a given key. + /// The cache key. + public bool ContainsKey(string key) + { + return this.Cache.ContainsKey(key); + } + + + /**** + ** Normalise + ****/ + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + string normalised = string.Join(this.PreferredPathSeparator, parts); + if (path.StartsWith(this.PreferredPathSeparator)) + normalised = this.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Normalise a cache key so it's consistent with the underlying cache. + /// The asset key. + [Pure] + public string NormaliseKey(string key) + { + key = this.NormalisePathSeparators(key); + return key.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase) + ? key.Substring(0, key.Length - 4) + : this.NormaliseAssetNameForPlatform(key); + } + + /**** + ** Remove + ****/ + /// Remove an asset with the given key. + /// The cache key. + /// Whether to dispose the entry value, if applicable. + /// Returns the removed key (if any). + public bool Remove(string key, bool dispose) + { + // get entry + if (!this.Cache.TryGetValue(key, out object value)) + return false; + + // dispose & remove entry + if (dispose && value is IDisposable disposable) + disposable.Dispose(); + + return this.Cache.Remove(key); + } + + /// 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 removed keys (if any). + public IEnumerable Remove(Func predicate, bool dispose = false) + { + List removed = new List(); + foreach (string key in this.Cache.Keys.ToArray()) + { + Type type = this.Cache[key].GetType(); + if (predicate(key, type)) + { + this.Remove(key, dispose); + removed.Add(key); + } + } + return removed; + } + } +} diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 2f5d104f..0b6daaa6 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.Xna.Framework; +using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; @@ -15,7 +15,17 @@ using StardewValley; namespace StardewModdingAPI.Framework { - /// SMAPI's implementation of the game's content manager which lets it raise content events. + /// A thread-safe content manager which intercepts assets being loaded to let SMAPI mods inject or edit them. + /// + /// This is the centralised content manager 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 SContentManager : LocalizedContentManager { /********* @@ -27,11 +37,8 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// The underlying content manager's asset cache. - private readonly IDictionary Cache; - - /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. - private readonly Func NormaliseAssetNameForPlatform; + /// The underlying asset cache. + private readonly ContentCache Cache; /// The private method which generates the locale portion of an asset name. private readonly IPrivateMethod GetKeyLocale; @@ -46,10 +53,10 @@ namespace StardewModdingAPI.Framework private readonly ContextHash AssetsBeingLoaded = new ContextHash(); /// A lookup of the content managers which loaded each asset. - private readonly IDictionary> AssetLoaders = new Dictionary>(); + private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>(); - /// An object locked to prevent concurrent changes to the underlying assets. - private readonly object Lock = new object(); + /// A lock used to prevents concurrent changes to the cache while data is being read. + private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); /********* @@ -77,30 +84,15 @@ namespace StardewModdingAPI.Framework /// The current culture for which to localise content. /// The current language code for which to localise content. /// Encapsulates monitoring and logging. - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) + /// Simplifies access to private code. + public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { - // validate - if (monitor == null) - throw new ArgumentNullException(nameof(monitor)); - - // initialise - var reflection = new Reflector(); - this.Monitor = monitor; - - // get underlying fields for interception - this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); + // init + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); - // get asset key normalisation logic - if (Constants.TargetPlatform == Platform.Windows) - { - IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); - this.NormaliseAssetNameForPlatform = path => method.Invoke(path); - } - else - this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic - // get asset data this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); @@ -108,34 +100,26 @@ namespace StardewModdingAPI.Framework /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. + [Pure] public string NormalisePathSeparators(string path) { - string[] parts = path.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - string normalised = string.Join(SContentManager.PreferredPathSeparator, parts); - if (path.StartsWith(SContentManager.PreferredPathSeparator)) - normalised = SContentManager.PreferredPathSeparator + normalised; // keep root slash - return normalised; + 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) { - assetName = this.NormalisePathSeparators(assetName); - if (assetName.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)) - return assetName.Substring(0, assetName.Length - 4); - return this.NormaliseAssetNameForPlatform(assetName); + return this.Cache.NormaliseKey(assetName); } /// 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) { - lock (this.Lock) - { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); - } + assetName = this.Cache.NormaliseKey(assetName); + return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName)); } /// Load an asset that has been processed by the content pipeline. @@ -152,10 +136,9 @@ namespace StardewModdingAPI.Framework /// The content manager instance for which to load the asset. public T LoadFor(string assetName, ContentManager instance) { - lock (this.Lock) + assetName = this.NormaliseAssetName(assetName); + return this.WithWriteLock(() => { - assetName = this.NormaliseAssetName(assetName); - // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) { @@ -186,7 +169,7 @@ namespace StardewModdingAPI.Framework this.Cache[assetName] = data; this.TrackAssetLoader(assetName, instance); return data; - } + }); } /// Inject an asset into the cache. @@ -195,12 +178,12 @@ namespace StardewModdingAPI.Framework /// The asset value. public void Inject(string assetName, T value) { - lock (this.Lock) + this.WithWriteLock(() => { assetName = this.NormaliseAssetName(assetName); this.Cache[assetName] = value; this.TrackAssetLoader(assetName, this); - } + }); } /// Get the current content locale. @@ -212,19 +195,11 @@ namespace StardewModdingAPI.Framework /// Get the cached asset keys. public IEnumerable GetAssetKeys() { - lock (this.Lock) - { - IEnumerable GetAllAssetKeys() - { - foreach (string cacheKey in this.Cache.Keys) - { - this.ParseCacheKey(cacheKey, out string assetKey, out string _); - yield return assetKey; - } - } - - return GetAllAssetKeys().Distinct(); - } + return this.WithReadLock(() => + this.Cache.Keys + .Select(this.GetAssetName) + .Distinct() + ); } /// Purge assets from the cache that match one of the interceptors. @@ -239,11 +214,12 @@ namespace StardewModdingAPI.Framework // 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((assetName, assetType) => { - // get asset metadata IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); // check loaders @@ -263,48 +239,44 @@ namespace StardewModdingAPI.Framework /// Returns whether any cache entries were invalidated. public bool InvalidateCache(Func predicate, bool dispose = false) { - lock (this.Lock) + return this.WithWriteLock(() => { - // find matching asset keys - HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string cacheKey in this.Cache.Keys) + // invalidate matching keys + HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => { - this.ParseCacheKey(cacheKey, out string assetKey, out _); - Type type = this.Cache[cacheKey].GetType(); - if (predicate(assetKey, type)) + this.ParseCacheKey(key, out string assetName, out _); + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) { - purgeAssetKeys.Add(assetKey); - purgeCacheKeys.Add(cacheKey); + removeAssetNames.Add(assetName); + removeKeys.Add(key); + return true; } - } + return false; + }); - // purge assets - foreach (string key in purgeCacheKeys) - { - if (dispose && this.Cache[key] is IDisposable disposable) - disposable.Dispose(); - this.Cache.Remove(key); - this.AssetLoaders.Remove(key); - } + // update reference tracking + foreach (string key in removeKeys) + this.ContentManagersByAssetKey.Remove(key); // reload core game assets int reloaded = 0; - foreach (string key in purgeAssetKeys) + foreach (string key in removeAssetNames) { if (this.CoreAssets.ReloadForKey(this, key)) reloaded++; } // report result - if (purgeCacheKeys.Any()) + if (removeKeys.Any()) { - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + 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; - } + }); } /// Dispose assets for the given content manager shim. @@ -313,15 +285,26 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - foreach (var entry in this.AssetLoaders) - entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.AssetLoaders[key].Any(), dispose: true); + this.WithWriteLock(() => + { + foreach (var entry in this.ContentManagersByAssetKey) + entry.Value.Remove(shim); + this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey[key].Any(), dispose: true); + }); } /********* ** Private methods *********/ + /// Dispose held resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); + base.Dispose(disposing); + } + /// Get whether an asset has already been loaded. /// The normalised asset name. private bool IsNormalisedKeyLoaded(string normalisedAssetName) @@ -335,8 +318,8 @@ namespace StardewModdingAPI.Framework /// The content manager that loaded the asset. private void TrackAssetLoader(string key, ContentManager manager) { - if (!this.AssetLoaders.TryGetValue(key, out HashSet hash)) - hash = this.AssetLoaders[key] = new HashSet(); + if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) + hash = this.ContentManagersByAssetKey[key] = new HashSet(); hash.Add(manager); } @@ -367,11 +350,19 @@ namespace StardewModdingAPI.Framework 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 key. + /// The original asset name. /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) { // handle localised key if (!string.IsNullOrWhiteSpace(cacheKey)) @@ -382,7 +373,7 @@ namespace StardewModdingAPI.Framework string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); if (this.KeyLocales.ContainsKey(suffix)) { - assetKey = cacheKey.Substring(0, lastSepIndex); + assetName = cacheKey.Substring(0, lastSepIndex); localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); return; } @@ -390,7 +381,7 @@ namespace StardewModdingAPI.Framework } // handle simple key - assetKey = cacheKey; + assetName = cacheKey; localeCode = null; } @@ -519,12 +510,51 @@ namespace StardewModdingAPI.Framework } } - /// Dispose held resources. - /// Whether the content manager is disposing (rather than finalising). - protected override void Dispose(bool disposing) + /// 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) { - this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - base.Dispose(disposing); + 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.EnterReadLock(); + return action(); + } + finally + { + this.Lock.ExitReadLock(); + } } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index ca19d726..c62c1393 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -179,7 +179,7 @@ namespace StardewModdingAPI.Framework // override content manager this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); - this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6f7c2b3f..605292b2 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -89,6 +89,7 @@ Properties\GlobalAssemblyInfo.cs + From 68e33c653ad780f75df6642b78feca015e8dbbb2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Oct 2017 19:27:00 -0400 Subject: [PATCH 11/59] suppress BeforeSave, AfterSave, and AfterDayStarted events during new-game intro (#374) --- src/SMAPI/Framework/SGame.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c62c1393..c886a4b7 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -240,6 +240,9 @@ namespace StardewModdingAPI.Framework return; } + /********* + ** Save events + suppress events during save + *********/ // While the game is writing to the save file in the background, mods can unexpectedly // fail since they don't have exclusive access to resources (e.g. collection changed // during enumeration errors). To avoid problems, events are not invoked while a save @@ -248,7 +251,7 @@ namespace StardewModdingAPI.Framework if (Context.IsSaving) { // raise before-save - if (!this.IsBetweenSaveEvents) + if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); From 749ebb912bc59fceda2f14d7e330cd9edeff19ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Oct 2017 19:54:38 -0400 Subject: [PATCH 12/59] fix inconsistent ASCII art letter sizes in readme Thanks to Raven on Discord! --- src/SMAPI.Installer/readme.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/SMAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt index eb27ac52..a03ad6a4 100644 --- a/src/SMAPI.Installer/readme.txt +++ b/src/SMAPI.Installer/readme.txt @@ -1,14 +1,14 @@ - ___ ___ ___ ___ - / /\ /__/\ / /\ / /\ ___ - / /:/_ | |::\ / /::\ / /::\ / /\ - / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ - / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ /__/::\ - /__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ \__\/\:\__ - \ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \ \:\/\ - \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ - \__\/ /:/ \ \:\ \ \:\ \ \:\ /__/:/ - /__/:/ \ \:\ \ \:\ \ \:\ \__\/ - \__\/ \__\/ \__\/ \__\/ + ___ ___ ___ ___ ___ + / /\ /__/\ / /\ / /\ / /\ + / /:/_ | |::\ / /::\ / /::\ / /:/ + / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ + / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ / /::\ ___ +/__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ /__/:/\:\ /\ +\ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \__\/ \:\/:/ + \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ + \__\/ /:/ \ \:\ \ \:\ \ \:\ / /:/ + /__/:/ \ \:\ \ \:\ \ \:\ /__/:/ + \__\/ \__\/ \__\/ \__\/ \__\/ SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. From ded647aad41d8e3591a21bdd6aa6503273312a27 Mon Sep 17 00:00:00 2001 From: Entoarox Date: Fri, 13 Oct 2017 18:19:04 +0200 Subject: [PATCH 13/59] PrivateProperty.cs ~ Use delegates for performance --- .../Framework/Reflection/PrivateProperty.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 08204b7e..8a75d925 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.Reflection /// The display name shown in error messages. private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; + private readonly Func GetterDelegate; + private readonly Action SetterDelegate; + /********* ** Accessors @@ -39,20 +42,17 @@ namespace StardewModdingAPI.Framework.Reflection /// The is null for a non-static field, or not null for a static field. public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { - // validate - if (parentType == null) - throw new ArgumentNullException(nameof(parentType)); - if (property == null) - throw new ArgumentNullException(nameof(property)); if (isStatic && obj != null) throw new ArgumentException("A static property cannot have an object instance."); if (!isStatic && obj == null) throw new ArgumentException("A non-static property must have an object instance."); - // save - this.ParentType = parentType; + this.ParentType = parentType ?? throw new ArgumentNullException(nameof(parentType)); this.Parent = obj; - this.PropertyInfo = property; + this.PropertyInfo = property ?? throw new ArgumentNullException(nameof(property)); + + this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), this.PropertyInfo.GetMethod); + this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), this.PropertyInfo.SetMethod); } /// Get the property value. @@ -60,7 +60,9 @@ namespace StardewModdingAPI.Framework.Reflection { try { - return (TValue)this.PropertyInfo.GetValue(this.Parent); + return this.GetterDelegate(this.Parent); + // Old version: Commented out in case of issues with new version + //return (TValue)this.PropertyInfo.GetValue(this.Parent); } catch (InvalidCastException) { @@ -78,7 +80,9 @@ namespace StardewModdingAPI.Framework.Reflection { try { - this.PropertyInfo.SetValue(this.Parent, value); + this.SetterDelegate(this.Parent, value); + // Old version: Commented out in case of issues with new version + //this.PropertyInfo.SetValue(this.Parent, value); } catch (InvalidCastException) { From 191d65f8d9e90cc3a9788afcae852f8879962428 Mon Sep 17 00:00:00 2001 From: Entoarox Date: Fri, 13 Oct 2017 19:00:55 +0200 Subject: [PATCH 14/59] Fix: Instance type is required --- src/SMAPI/Framework/Reflection/PrivateProperty.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 8a75d925..718594ee 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -51,8 +51,10 @@ namespace StardewModdingAPI.Framework.Reflection this.Parent = obj; this.PropertyInfo = property ?? throw new ArgumentNullException(nameof(property)); - this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), this.PropertyInfo.GetMethod); - this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), this.PropertyInfo.SetMethod); + Type[] types = new Type[] { this.PropertyInfo.DeclaringType, typeof(TValue)}; + + this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func<,>).MakeGenericType(types), this.PropertyInfo.GetMethod); + this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action<,>).MakeGenericType(types), this.PropertyInfo.SetMethod); } /// Get the property value. From 7e02310a8ea9c24607a88718ee10ac5f85836fdb Mon Sep 17 00:00:00 2001 From: Entoarox Date: Mon, 23 Oct 2017 18:15:18 +0200 Subject: [PATCH 15/59] Fix object cast being needed - use closed instead of open delegate The API does not allow the user to modify the `this` after the fact anyhow, so it isnt needed. --- src/SMAPI/Framework/Reflection/PrivateProperty.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 718594ee..0fa10601 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Framework.Reflection /// The display name shown in error messages. private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; - private readonly Func GetterDelegate; - private readonly Action SetterDelegate; + private readonly Func GetterDelegate; + private readonly Action SetterDelegate; /********* @@ -53,8 +53,8 @@ namespace StardewModdingAPI.Framework.Reflection Type[] types = new Type[] { this.PropertyInfo.DeclaringType, typeof(TValue)}; - this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func<,>).MakeGenericType(types), this.PropertyInfo.GetMethod); - this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action<,>).MakeGenericType(types), this.PropertyInfo.SetMethod); + this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); + this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); } /// Get the property value. @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - return this.GetterDelegate(this.Parent); + return this.GetterDelegate(); // Old version: Commented out in case of issues with new version //return (TValue)this.PropertyInfo.GetValue(this.Parent); } @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - this.SetterDelegate(this.Parent, value); + this.SetterDelegate(value); // Old version: Commented out in case of issues with new version //this.PropertyInfo.SetValue(this.Parent, value); } From f6a86e584976c87f1f678a226f8eafe6a8b9860c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Oct 2017 20:28:18 -0400 Subject: [PATCH 16/59] minor cleanup --- .../Framework/Reflection/PrivateProperty.cs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 0fa10601..be346d71 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; namespace StardewModdingAPI.Framework.Reflection @@ -10,16 +10,13 @@ namespace StardewModdingAPI.Framework.Reflection /********* ** Properties *********/ - /// The type that has the field. - private readonly Type ParentType; - - /// The object that has the instance field (if applicable). - private readonly object Parent; - /// The display name shown in error messages. - private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; + private readonly string DisplayName; + /// The underlying property getter. private readonly Func GetterDelegate; + + /// The underlying property setter. private readonly Action SetterDelegate; @@ -42,16 +39,21 @@ namespace StardewModdingAPI.Framework.Reflection /// The is null for a non-static field, or not null for a static field. public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { + // validate input + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (property == null) + throw new ArgumentNullException(nameof(property)); + + // validate static if (isStatic && obj != null) throw new ArgumentException("A static property cannot have an object instance."); if (!isStatic && obj == null) throw new ArgumentException("A non-static property must have an object instance."); - this.ParentType = parentType ?? throw new ArgumentNullException(nameof(parentType)); - this.Parent = obj; - this.PropertyInfo = property ?? throw new ArgumentNullException(nameof(property)); - Type[] types = new Type[] { this.PropertyInfo.DeclaringType, typeof(TValue)}; + this.DisplayName = $"{parentType.FullName}::{property.Name}"; + this.PropertyInfo = property; this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); @@ -63,8 +65,6 @@ namespace StardewModdingAPI.Framework.Reflection try { return this.GetterDelegate(); - // Old version: Commented out in case of issues with new version - //return (TValue)this.PropertyInfo.GetValue(this.Parent); } catch (InvalidCastException) { @@ -83,8 +83,6 @@ namespace StardewModdingAPI.Framework.Reflection try { this.SetterDelegate(value); - // Old version: Commented out in case of issues with new version - //this.PropertyInfo.SetValue(this.Parent, value); } catch (InvalidCastException) { From a1eeece49b937c942e2cc002bd1863295d943fde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Oct 2017 17:14:58 -0400 Subject: [PATCH 17/59] centralise most content-loading logic to fix map tilesheet edge case (#373) --- .../Framework/ModHelpers/ContentHelper.cs | 185 ++------ src/SMAPI/Framework/SContentManager.cs | 406 ++++++++++++++---- src/SMAPI/IContentHelper.cs | 3 +- 3 files changed, 348 insertions(+), 246 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4f5bd2f0..2dd8a2e3 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; @@ -74,12 +72,12 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; - this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath); this.Monitor = monitor; } /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. /// Where to search for a matching content asset. /// The is empty or contains invalid characters. @@ -88,9 +86,9 @@ namespace StardewModdingAPI.Framework.ModHelpers { SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); - this.AssertValidAssetKeyFormat(key); try { + this.ContentManager.AssertValidAssetKeyFormat(key); switch (source) { case ContentSource.GameContent: @@ -103,60 +101,32 @@ namespace StardewModdingAPI.Framework.ModHelpers throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path - string assetPath = this.GetModAssetPath(key, file.FullName); + string assetName = this.GetModAssetPath(key, file.FullName); // try cache - if (this.ContentManager.IsLoaded(assetPath)) - return this.ContentManager.Load(assetPath); + if (this.ContentManager.IsLoaded(assetName)) + return this.ContentManager.Load(assetName); - // load content - switch (file.Extension.ToLower()) + // fix map tilesheets + if (file.Extension.ToLower() == ".tbin") { - // XNB file - case ".xnb": - { - T asset = this.ContentManager.Load(assetPath); - if (asset is Map) - this.FixLocalMapTilesheets(asset as Map, key); - return asset; - } + // validate + if (typeof(T) != typeof(Map)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); - // unpacked map - case ".tbin": - { - // validate - if (typeof(T) != typeof(Map)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + this.FixLocalMapTilesheets(map, key); - // fetch & cache - FormatManager formatManager = FormatManager.Instance; - Map map = formatManager.LoadMap(file.FullName); - this.FixLocalMapTilesheets(map, key); - - // inject map - this.ContentManager.Inject(assetPath, map); - return (T)(object)map; - } - - // 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.ContentManager.Inject(assetPath, texture); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + // inject map + this.ContentManager.Inject(assetName, map, this.ContentManager); + return (T)(object)map; } + // load through content manager + return this.ContentManager.Load(assetName); + default: throw GetContentError($"unknown content source '{source}'."); } @@ -264,8 +234,8 @@ namespace StardewModdingAPI.Framework.ModHelpers try { string key = - this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource) - ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource); + this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource) + ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource); if (key != null) { tilesheet.ImageSource = key; @@ -282,33 +252,22 @@ namespace StardewModdingAPI.Framework.ModHelpers } } - /// Load a tilesheet image source if the file exists. - /// The folder path containing the map, relative to the mod folder. + /// Get the actual asset name for a tilesheet. + /// The folder path containing the map, relative to the mod folder. /// The tilesheet image source to load. - /// Returns the loaded asset key (if it was loaded successfully). + /// Returns the asset name. /// See remarks on . - private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource) + private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource) { if (imageSource == null) return null; // check relative to map file { - string localKey = Path.Combine(relativeMapFolder, imageSource); + string localKey = Path.Combine(modRelativeMapFolder, imageSource); FileInfo localFile = this.GetModFile(localKey); if (localFile.Exists) - { - try - { - this.Load(localKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex); - } - return this.GetActualAssetKey(localKey); - } } // check relative to content folder @@ -343,18 +302,6 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - /// 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", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")] - private 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."); - } - /// Get a file from the mod folder. /// The asset path relative to the mod folder. private FileInfo GetModFile(string path) @@ -400,81 +347,5 @@ namespace StardewModdingAPI.Framework.ModHelpers return absolutePath; #endif } - - /// Get a directory path relative to a given root. - /// The root path from which the path should be relative. - /// The target file path. - private string GetRelativePath(string rootPath, string targetPath) - { - // convert to URIs - Uri from = new Uri(rootPath + "/"); - Uri to = new Uri(targetPath + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); - - // get relative path - return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) - .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform - } - - /// 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/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 0b6daaa6..10d854d9 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -1,13 +1,17 @@ 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; @@ -55,6 +59,9 @@ namespace StardewModdingAPI.Framework /// 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); @@ -78,6 +85,9 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ + /**** + ** Constructor + ****/ /// Construct an instance. /// The service provider to use to locate services. /// The root directory to search for content. @@ -92,12 +102,16 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); + this.ModContentPrefix = this.GetRelativePath(Constants.ModPath); // get asset data this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); } + /**** + ** Asset key/name handling + ****/ /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] @@ -114,6 +128,42 @@ namespace StardewModdingAPI.Framework 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."); + } + + /// Get a directory path relative to the content root. + /// The target file path. + public string GetRelativePath(string targetPath) + { + // convert to URIs + Uri from = new Uri(this.FullRootDirectory + "/"); + Uri to = new Uri(targetPath + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'."); + + // get relative path + return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) + .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetKeyLocale.Invoke(); + } + /// 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) @@ -122,76 +172,6 @@ namespace StardewModdingAPI.Framework return this.WithReadLock(() => this.IsNormalisedKeyLoaded(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. - public override T Load(string assetName) - { - return this.LoadFor(assetName, this); - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The content manager instance for which to load the asset. - public T LoadFor(string assetName, ContentManager instance) - { - assetName = this.NormaliseAssetName(assetName); - return this.WithWriteLock(() => - { - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - { - this.TrackAssetLoader(assetName, instance); - return base.Load(assetName); - } - - // 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); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => - { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } - - // update cache & return data - this.Cache[assetName] = data; - this.TrackAssetLoader(assetName, instance); - return data; - }); - } - - /// 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.WithWriteLock(() => - { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - this.TrackAssetLoader(assetName, this); - }); - } - - /// Get the current content locale. - public string GetLocale() - { - return this.GetKeyLocale.Invoke(); - } - /// Get the cached asset keys. public IEnumerable GetAssetKeys() { @@ -202,6 +182,95 @@ namespace StardewModdingAPI.Framework ); } + /// 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. + public override T Load(string assetName) + { + return this.LoadFor(assetName, this); + } + + /// 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 is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T LoadFor(string assetName, ContentManager instance) + { + // normalise asset key + this.AssertValidAssetKeyFormat(assetName); + assetName = this.NormaliseAssetName(assetName); + + // load game content + if (!assetName.StartsWith(this.ModContentPrefix)) + return this.LoadImpl(assetName, instance); + + // 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); + + // 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); + + // 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)) + { + 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. @@ -279,6 +348,9 @@ namespace StardewModdingAPI.Framework }); } + /**** + ** Disposal + ****/ /// Dispose assets for the given content manager shim. /// The content manager whose assets to dispose. internal void DisposeFor(ContentManagerShim shim) @@ -297,6 +369,9 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /**** + ** Disposal + ****/ /// Dispose held resources. /// Whether the content manager is disposing (rather than finalising). protected override void Dispose(bool disposing) @@ -305,24 +380,9 @@ namespace StardewModdingAPI.Framework base.Dispose(disposing); } - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - return this.Cache.ContainsKey(normalisedAssetName) - || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset - } - - /// Track that a content manager loaded an asset. - /// The asset key that was loaded. - /// The content manager that loaded the asset. - private void TrackAssetLoader(string key, ContentManager manager) - { - if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) - hash = this.ContentManagersByAssetKey[key] = new HashSet(); - hash.Add(manager); - } - + /**** + ** Asset name/key handling + ****/ /// Get the locale codes (like ja-JP) used in asset keys. /// Simplifies access to private game code. private IDictionary GetKeyLocales(Reflector reflection) @@ -385,6 +445,113 @@ namespace StardewModdingAPI.Framework localeCode = null; } + /**** + ** Cache handling + ****/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + private bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName) + || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset + } + + /// Track that a content manager loaded an asset. + /// The asset key that was loaded. + /// The content manager that loaded the asset. + private void TrackAssetLoader(string key, ContentManager manager) + { + if (!this.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. + private T LoadImpl(string assetName, ContentManager instance) + { + return this.WithWriteLock(() => + { + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) + { + this.TrackAssetLoader(assetName, instance); + return base.Load(assetName); + } + + // 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); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), 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(path + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Get a file from the game's content folder. + /// The asset key. + private FileInfo GetContentFolderFile(string key) + { + // get file path + string path = Path.Combine(this.FullRootDirectory, key); + if (!path.EndsWith(".xnb")) + path += ".xnb"; + + // get file + return new FileInfo(path); + } + /// Load the initial asset from the registered . /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. @@ -510,6 +677,69 @@ namespace StardewModdingAPI.Framework } } + /// 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. diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index b78b165b..7900809f 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewValley; +using xTile; namespace StardewModdingAPI { @@ -29,7 +30,7 @@ namespace StardewModdingAPI ** Public methods *********/ /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. /// Where to search for a matching content asset. /// The is empty or contains invalid characters. From a7fcfd642466b22abdc32a1f71a93e77fb8e569b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:00:03 -0400 Subject: [PATCH 18/59] fix incorrect search path when loading a mod file (#373) --- src/SMAPI/Framework/SContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 10d854d9..54ebba83 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -531,7 +531,7 @@ namespace StardewModdingAPI.Framework // try with default extension if (!file.Exists && file.Extension.ToLower() != ".xnb") { - FileInfo result = new FileInfo(path + ".xnb"); + FileInfo result = new FileInfo(file.FullName + ".xnb"); if (result.Exists) file = result; } From 801f25a51efbed0b8b16e6b9e8f1c543fcc45c47 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:01:55 -0400 Subject: [PATCH 19/59] update release notes (#373) --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 452fd40a..a06fc0c4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * Deprecated `e.IsClick`, which is limited and unclear. Use `IsActionButton` or `IsUseToolButton` instead. * Fixed `e.SuppressButton()` not correctly suppressing keyboard buttons. * Fixed `e.IsClick` (now `e.IsActionButton`) ignoring custom key bindings. + * Fixed custom map tilesheets not working unless they're explicitly loaded first. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. * Fixed SMAPI blocking reflection access to vanilla members on overridden types. From f63484e5e76306a08e2f2f2c2f1224cc6b0af1ba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:17:25 -0400 Subject: [PATCH 20/59] minor cleanup (#373) --- .../Framework/ModHelpers/ContentHelper.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 2dd8a2e3..ae812e71 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -117,7 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - this.FixLocalMapTilesheets(map, key); + this.FixCustomTilesheetPaths(map, key); // inject map this.ContentManager.Inject(assetName, map, this.ContentManager); @@ -180,25 +180,27 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Private methods *********/ - /// Fix the tilesheets for a map loaded from the mod folder. + /// 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 map tilesheets could not be loaded. + /// A map tilesheet couldn't be resolved. /// - /// The game's logic for tilesheets in is a bit specialised. It boils down to this: - /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the Content folder. + /// The game's logic for tilesheets in is a bit specialised. It boils + /// down to this: + /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded + /// as-is relative to the Content folder. /// * Else it's loaded from Content\Maps with a seasonal prefix. /// /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. /// Instead we use a more heuristic approach: check relative to the map file first, then relative to - /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, we try - /// for a seasonal variation and then an exact match. + /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, try for a + /// seasonal variation and then an exact match. /// /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. /// - private void FixLocalMapTilesheets(Map map, string mapKey) + private void FixCustomTilesheetPaths(Map map, string mapKey) { - // check map info + // get map info if (!map.TileSheets.Any()) return; mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators @@ -209,7 +211,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string imageSource = tilesheet.ImageSource; - // validate + // validate tilesheet path if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains("..")) throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); @@ -256,7 +258,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The folder path containing the map, relative to the mod folder. /// The tilesheet image source to load. /// Returns the asset name. - /// See remarks on . + /// See remarks on . private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource) { if (imageSource == null) @@ -286,7 +288,7 @@ namespace StardewModdingAPI.Framework.ModHelpers catch { // ignore file-not-found errors - // TODO: while it's useful to suppress a asset-not-found error here to avoid + // TODO: while it's useful to suppress an asset-not-found error here to avoid // confusion, this is a pretty naive approach. Even if the file doesn't exist, // the file may have been loaded through an IAssetLoader which failed. So even // if the content file doesn't exist, that doesn't mean the error here is a From 7f16ebdb19982c182b60312883452c44fdd08fda Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:42:54 -0400 Subject: [PATCH 21/59] hide the game's test messages from the console & log (#364) --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index a06fc0c4..ba0815b3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.1 (upcoming) * For players: * Fixed compatibility check crashing for players with Stardew Valley 1.08. + * Fixed the game's test messages being shown in the console and log. * For modders: * Added support for public code in reflection API, to simplify mod integrations. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index fe306e24..ce547d9b 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS using System.Management; @@ -77,6 +78,13 @@ namespace StardewModdingAPI /// Whether the program has been disposed. private bool IsDisposed; + /// Regex patterns which match console messages to suppress from the console and log. + 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) + }; + /********* ** Public methods @@ -910,7 +918,14 @@ namespace StardewModdingAPI /// The message to log. private void HandleConsoleMessage(IMonitor monitor, string message) { - LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; // intercept potential exceptions + // detect exception + LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; + + // ignore suppressed message + if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) + return; + + // forward to monitor monitor.Log(message, level); } From b945fcf5553f2df63db1fad8a73c65cd7fa7daa3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 02:44:53 -0400 Subject: [PATCH 22/59] fix player_setlevel command not also changing XP (#359) --- docs/release-notes.md | 1 + .../Commands/Player/SetLevelCommand.cs | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index ba0815b3..9366e1fc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * For players: * Fixed compatibility check crashing for players with Stardew Valley 1.08. * Fixed the game's test messages being shown in the console and log. + * Fixed TrainerMod's `player_setlevel` command not also setting XP. * For modders: * Added support for public code in reflection API, to simplify mod integrations. diff --git a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs index b223aa9f..54d5e47b 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs +++ b/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs @@ -1,11 +1,34 @@ -using StardewModdingAPI; +using System.Collections.Generic; +using StardewModdingAPI; using StardewValley; +using SFarmer = StardewValley.Farmer; namespace TrainerMod.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 *********/ @@ -30,31 +53,37 @@ namespace TrainerMod.Framework.Commands.Player { case "luck": Game1.player.LuckLevel = level; + Game1.player.experiencePoints[SFarmer.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]; 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]; 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]; 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]; 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]; monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info); break; } From 59dd604cf2905adf5fce7e9bb7b97886891aae81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 03:18:48 -0400 Subject: [PATCH 23/59] rename TrainerMod to Console Commands to clarify purpose --- build/common.targets | 10 +++++----- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 1 + .../ConsoleCommandsMod.cs} | 11 +++++------ .../Framework/Commands/ArgumentParser.cs | 3 +-- .../Framework/Commands/ITrainerCommand.cs | 6 ++---- .../Framework/Commands/Other/DebugCommand.cs | 5 ++--- .../Framework/Commands/Other/ShowDataFilesCommand.cs | 3 +-- .../Framework/Commands/Other/ShowGameFilesCommand.cs | 3 +-- .../Framework/Commands/Player/AddCommand.cs | 5 ++--- .../Framework/Commands/Player/ListItemTypesCommand.cs | 5 ++--- .../Framework/Commands/Player/ListItemsCommand.cs | 5 ++--- .../Framework/Commands/Player/SetColorCommand.cs | 3 +-- .../Framework/Commands/Player/SetHealthCommand.cs | 3 +-- .../Framework/Commands/Player/SetImmunityCommand.cs | 3 +-- .../Framework/Commands/Player/SetLevelCommand.cs | 3 +-- .../Framework/Commands/Player/SetMaxHealthCommand.cs | 3 +-- .../Framework/Commands/Player/SetMaxStaminaCommand.cs | 3 +-- .../Framework/Commands/Player/SetMoneyCommand.cs | 3 +-- .../Framework/Commands/Player/SetNameCommand.cs | 5 ++--- .../Framework/Commands/Player/SetSpeedCommand.cs | 5 ++--- .../Framework/Commands/Player/SetStaminaCommand.cs | 3 +-- .../Framework/Commands/Player/SetStyleCommand.cs | 5 ++--- .../Framework/Commands/TrainerCommand.cs | 3 +-- .../Framework/Commands/World/DownMineLevelCommand.cs | 5 ++--- .../Framework/Commands/World/FreezeTimeCommand.cs | 3 +-- .../Framework/Commands/World/SetDayCommand.cs | 3 +-- .../Framework/Commands/World/SetMineLevelCommand.cs | 3 +-- .../Framework/Commands/World/SetSeasonCommand.cs | 3 +-- .../Framework/Commands/World/SetTimeCommand.cs | 3 +-- .../Framework/Commands/World/SetYearCommand.cs | 3 +-- .../Framework/ItemData/ItemType.cs | 2 +- .../Framework/ItemData/SearchableItem.cs | 2 +- .../Framework/ItemRepository.cs | 4 ++-- .../Properties/AssemblyInfo.cs | 6 ++++++ .../StardewModdingAPI.Mods.ConsoleCommands.csproj} | 10 +++++----- .../manifest.json | 6 +++--- .../packages.config | 0 src/SMAPI.sln | 4 ++-- src/TrainerMod/Properties/AssemblyInfo.cs | 6 ------ 40 files changed, 68 insertions(+), 95 deletions(-) rename src/{TrainerMod/TrainerMod.cs => SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs} (91%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/ArgumentParser.cs (98%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/ITrainerCommand.cs (88%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Other/DebugCommand.cs (92%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Other/ShowDataFilesCommand.cs (92%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Other/ShowGameFilesCommand.cs (92%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/AddCommand.cs (95%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/ListItemTypesCommand.cs (92%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/ListItemsCommand.cs (95%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetColorCommand.cs (97%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetHealthCommand.cs (97%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetImmunityCommand.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetLevelCommand.cs (98%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetMaxHealthCommand.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetMaxStaminaCommand.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetMoneyCommand.cs (97%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetNameCommand.cs (95%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetSpeedCommand.cs (92%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetStaminaCommand.cs (97%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/Player/SetStyleCommand.cs (97%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/TrainerCommand.cs (98%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/DownMineLevelCommand.cs (91%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/FreezeTimeCommand.cs (97%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/SetDayCommand.cs (95%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/SetMineLevelCommand.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/SetSeasonCommand.cs (95%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/SetTimeCommand.cs (95%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/Commands/World/SetYearCommand.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/ItemData/ItemType.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/ItemData/SearchableItem.cs (94%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/Framework/ItemRepository.cs (98%) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs rename src/{TrainerMod/TrainerMod.csproj => SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj} (93%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/manifest.json (67%) rename src/{TrainerMod => SMAPI.Mods.ConsoleCommands}/packages.config (100%) delete mode 100644 src/TrainerMod/Properties/AssemblyInfo.cs diff --git a/build/common.targets b/build/common.targets index ee138524..aa11344e 100644 --- a/build/common.targets +++ b/build/common.targets @@ -78,7 +78,7 @@ - + @@ -89,10 +89,10 @@ - - - - + + + + diff --git a/docs/release-notes.md b/docs/release-notes.md index 9366e1fc..1202407f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ * Fixed compatibility check crashing for players with Stardew Valley 1.08. * Fixed the game's test messages being shown in the console and log. * Fixed TrainerMod's `player_setlevel` command not also setting XP. + * Renamed the default _TrainerMod_ mod to _Console Commands_ to clarify its purpose. * For modders: * Added support for public code in reflection API, to simplify mod integrations. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 1a132e54..cbc8a401 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -97,6 +97,7 @@ namespace StardewModdingApi.Installer // obsolete yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 + yield return GetInstallPath("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 if (modsDir.Exists) diff --git a/src/TrainerMod/TrainerMod.cs b/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs similarity index 91% rename from src/TrainerMod/TrainerMod.cs rename to src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs index 5db02cd6..96658928 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs @@ -1,14 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI; using StardewModdingAPI.Events; -using TrainerMod.Framework.Commands; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; -namespace TrainerMod +namespace StardewModdingAPI.Mods.ConsoleCommands { /// The main entry point for the mod. - public class TrainerMod : Mod + public class ConsoleCommandsMod : Mod { /********* ** Properties @@ -52,7 +51,7 @@ namespace TrainerMod } } - /// Handle a TrainerMod command. + /// Handle a console command. /// The command to invoke. /// The command name specified by the user. /// The command arguments. diff --git a/src/TrainerMod/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs similarity index 98% rename from src/TrainerMod/Framework/Commands/ArgumentParser.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 6bcd3ff8..3ad1e168 100644 --- a/src/TrainerMod/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -2,9 +2,8 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI; -namespace TrainerMod.Framework.Commands +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands { /// Provides methods for parsing command-line arguments. internal class ArgumentParser : IReadOnlyList diff --git a/src/TrainerMod/Framework/Commands/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs similarity index 88% rename from src/TrainerMod/Framework/Commands/ITrainerCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs index 3d97e799..a0b739f8 100644 --- a/src/TrainerMod/Framework/Commands/ITrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs @@ -1,8 +1,6 @@ -using StardewModdingAPI; - -namespace TrainerMod.Framework.Commands +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands { - /// A TrainerMod command to register. + /// A console command to register. internal interface ITrainerCommand { /********* diff --git a/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs similarity index 92% rename from src/TrainerMod/Framework/Commands/Other/DebugCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs index 8c6e9f3b..e4010111 100644 --- a/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs @@ -1,7 +1,6 @@ -using StardewModdingAPI; -using StardewValley; +using StardewValley; -namespace TrainerMod.Framework.Commands.Other +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which sends a debug command to the game. internal class DebugCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs similarity index 92% rename from src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs index 367a70c6..54d27185 100644 --- a/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs @@ -1,7 +1,6 @@ using System.Diagnostics; -using StardewModdingAPI; -namespace TrainerMod.Framework.Commands.Other +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which shows the data files. internal class ShowDataFilesCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs similarity index 92% rename from src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs index 67fa83a3..0257892f 100644 --- a/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs @@ -1,7 +1,6 @@ using System.Diagnostics; -using StardewModdingAPI; -namespace TrainerMod.Framework.Commands.Other +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which shows the game files. internal class ShowGameFilesCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs similarity index 95% rename from src/TrainerMod/Framework/Commands/Player/AddCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 47840202..81167747 100644 --- a/src/TrainerMod/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -1,11 +1,10 @@ using System; using System.Linq; -using StardewModdingAPI; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; -using TrainerMod.Framework.ItemData; using Object = StardewValley.Object; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which adds an item to the player inventory. internal class AddCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs similarity index 92% rename from src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs index 5f14edbb..34f1760c 100644 --- a/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; -using TrainerMod.Framework.ItemData; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which list item types. internal class ListItemTypesCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs similarity index 95% rename from src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index 7f4f454c..942a50b8 100644 --- a/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI; -using TrainerMod.Framework.ItemData; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which list items available to spawn. internal class ListItemsCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs similarity index 97% rename from src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index 28ace0df..5d098593 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -1,8 +1,7 @@ using Microsoft.Xna.Framework; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the color of a player feature. internal class SetColorCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs similarity index 97% rename from src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index f64e9035..2e8f6630 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current health. internal class SetHealthCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs similarity index 94% rename from src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs index 59b28a3c..9c66c4fe 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current immunity. internal class SetImmunityCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs similarity index 98% rename from src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs index 54d5e47b..68891267 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using StardewModdingAPI; using StardewValley; using SFarmer = StardewValley.Farmer; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current level for a skill. internal class SetLevelCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs similarity index 94% rename from src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs index 4b9d87dc..f4ae0694 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's maximum health. internal class SetMaxHealthCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs similarity index 94% rename from src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs index 3997bb1b..5bce5ea3 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's maximum stamina. internal class SetMaxStaminaCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs similarity index 97% rename from src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 55e069a4..3fc504b1 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current money. internal class SetMoneyCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs similarity index 95% rename from src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs index 3fd4475c..5b1225e8 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -1,7 +1,6 @@ -using StardewModdingAPI; -using StardewValley; +using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's name. internal class SetNameCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs similarity index 92% rename from src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs index 40b87b62..e9693540 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs @@ -1,7 +1,6 @@ -using StardewModdingAPI; -using StardewValley; +using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current added speed. internal class SetSpeedCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs similarity index 97% rename from src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index d44d1370..866c3d22 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current stamina. internal class SetStaminaCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs similarity index 97% rename from src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs index 96e34af2..b59be2e5 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -1,7 +1,6 @@ -using StardewModdingAPI; -using StardewValley; +using StardewValley; -namespace TrainerMod.Framework.Commands.Player +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits a player style. internal class SetStyleCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs similarity index 98% rename from src/TrainerMod/Framework/Commands/TrainerCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index abe9ee41..466b8f6e 100644 --- a/src/TrainerMod/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI; -namespace TrainerMod.Framework.Commands +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands { /// The base implementation for a trainer command. internal abstract class TrainerCommand : ITrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs similarity index 91% rename from src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs index 4e62cf77..da117006 100644 --- a/src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs @@ -1,8 +1,7 @@ -using StardewModdingAPI; -using StardewValley; +using StardewValley; using StardewValley.Locations; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which moves the player to the next mine level. internal class DownMineLevelCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs similarity index 97% rename from src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 13d08398..2627b714 100644 --- a/src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which freezes the current time. internal class FreezeTimeCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs similarity index 95% rename from src/TrainerMod/Framework/Commands/World/SetDayCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs index 54267384..8d6bd759 100644 --- a/src/TrainerMod/Framework/Commands/World/SetDayCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current day. internal class SetDayCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs similarity index 94% rename from src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs index 225ec091..1024b7b6 100644 --- a/src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -1,8 +1,7 @@ using System; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which moves the player to the given mine level. internal class SetMineLevelCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs similarity index 95% rename from src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index 96c3d920..897d052f 100644 --- a/src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current season. internal class SetSeasonCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs similarity index 95% rename from src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index c827ea5e..d6c71387 100644 --- a/src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current time. internal class SetTimeCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs similarity index 94% rename from src/TrainerMod/Framework/Commands/World/SetYearCommand.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs index 760fc170..66abd6dc 100644 --- a/src/TrainerMod/Framework/Commands/World/SetYearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -1,8 +1,7 @@ using System.Linq; -using StardewModdingAPI; using StardewValley; -namespace TrainerMod.Framework.Commands.World +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current year. internal class SetYearCommand : TrainerCommand diff --git a/src/TrainerMod/Framework/ItemData/ItemType.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs similarity index 94% rename from src/TrainerMod/Framework/ItemData/ItemType.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs index 423455e9..797d4650 100644 --- a/src/TrainerMod/Framework/ItemData/ItemType.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs @@ -1,4 +1,4 @@ -namespace TrainerMod.Framework.ItemData +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData { /// An item type that can be searched and added to the player through the console. internal enum ItemType diff --git a/src/TrainerMod/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs similarity index 94% rename from src/TrainerMod/Framework/ItemData/SearchableItem.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs index 146da1a8..3eede413 100644 --- a/src/TrainerMod/Framework/ItemData/SearchableItem.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs @@ -1,6 +1,6 @@ using StardewValley; -namespace TrainerMod.Framework.ItemData +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData { /// A game item with metadata. internal class SearchableItem diff --git a/src/TrainerMod/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs similarity index 98% rename from src/TrainerMod/Framework/ItemRepository.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 96d3159e..b5fe9f2f 100644 --- a/src/TrainerMod/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; using StardewValley.Objects; using StardewValley.Tools; -using TrainerMod.Framework.ItemData; using SObject = StardewValley.Object; -namespace TrainerMod.Framework +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework { /// Provides methods for searching and constructing items. internal class ItemRepository diff --git a/src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs b/src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ac15ec72 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SMAPI.Mods.ConsoleCommands")] +[assembly: AssemblyDescription("")] +[assembly: Guid("76791e28-b1b5-407c-82d6-50c3e5b7e037")] diff --git a/src/TrainerMod/TrainerMod.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj similarity index 93% rename from src/TrainerMod/TrainerMod.csproj rename to src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index cb5ec47e..437d0986 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -7,8 +7,8 @@ {28480467-1A48-46A7-99F8-236D95225359} Library Properties - TrainerMod - TrainerMod + StardewModdingAPI.Mods.ConsoleCommands + ConsoleCommands v4.5 512 @@ -16,7 +16,7 @@ true full true - $(SolutionDir)\..\bin\Debug\Mods\TrainerMod\ + $(SolutionDir)\..\bin\Debug\Mods\ConsoleCommands\ DEBUG;TRACE prompt 4 @@ -27,7 +27,7 @@ pdbonly true - $(SolutionDir)\..\bin\Release\Mods\TrainerMod\ + $(SolutionDir)\..\bin\Release\Mods\ConsoleCommands\ TRACE prompt 4 @@ -80,7 +80,7 @@ - + diff --git a/src/TrainerMod/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json similarity index 67% rename from src/TrainerMod/manifest.json rename to src/SMAPI.Mods.ConsoleCommands/manifest.json index 22e35bce..664dfabf 100644 --- a/src/TrainerMod/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,5 +1,5 @@ { - "Name": "Trainer Mod", + "Name": "Console Commands", "Author": "SMAPI", "Version": { "MajorVersion": 2, @@ -8,6 +8,6 @@ "Build": null }, "Description": "Adds SMAPI console commands that let you manipulate the game.", - "UniqueID": "SMAPI.TrainerMod", - "EntryDll": "TrainerMod.dll" + "UniqueID": "SMAPI.ConsoleCommands", + "EntryDll": "ConsoleCommands.dll" } diff --git a/src/TrainerMod/packages.config b/src/SMAPI.Mods.ConsoleCommands/packages.config similarity index 100% rename from src/TrainerMod/packages.config rename to src/SMAPI.Mods.ConsoleCommands/packages.config diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 89a8d45c..8d730f37 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.16 +VisualStudioVersion = 15.0.27004.2002 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}" EndProject diff --git a/src/TrainerMod/Properties/AssemblyInfo.cs b/src/TrainerMod/Properties/AssemblyInfo.cs deleted file mode 100644 index 0b19e78a..00000000 --- a/src/TrainerMod/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("TrainerMod")] -[assembly: AssemblyDescription("")] -[assembly: Guid("76791e28-b1b5-407c-82d6-50c3e5b7e037")] \ No newline at end of file From 3d8bdacc8cb5c9d5514e052d5d4c1d5f2dbc6e9e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 03:19:21 -0400 Subject: [PATCH 24/59] fix ConsoleCommands mod including Json.NET DLL --- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index 437d0986..f228bb25 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -38,6 +38,7 @@ ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + False From 65f0fa625575592639a24a9b39330e4a6b500f22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:36:31 -0400 Subject: [PATCH 25/59] add scaffolding for web UI (#358) --- ...ModsController.cs => ModsApiController.cs} | 6 +- src/SMAPI.Web/Startup.cs | 1 + src/SMAPI.Web/Views/Shared/_Layout.cshtml | 29 +++++ src/SMAPI.Web/Views/_ViewStart.cshtml | 3 + src/SMAPI.Web/wwwroot/Content/main.css | 107 ++++++++++++++++++ src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif | Bin 0 -> 1104 bytes src/SMAPI.Web/wwwroot/favicon.ico | Bin 0 -> 15086 bytes 7 files changed, 143 insertions(+), 3 deletions(-) rename src/SMAPI.Web/Controllers/{ModsController.cs => ModsApiController.cs} (97%) create mode 100644 src/SMAPI.Web/Views/Shared/_Layout.cshtml create mode 100644 src/SMAPI.Web/Views/_ViewStart.cshtml create mode 100644 src/SMAPI.Web/wwwroot/Content/main.css create mode 100644 src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif create mode 100644 src/SMAPI.Web/wwwroot/favicon.ico diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs similarity index 97% rename from src/SMAPI.Web/Controllers/ModsController.cs rename to src/SMAPI.Web/Controllers/ModsApiController.cs index a671ddca..1db5b59e 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -14,8 +14,8 @@ namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] - [Route("api/{version:semanticVersion}/[controller]")] - internal class ModsController : Controller + [Route("api/{version:semanticVersion}/mods")] + internal class ModsApiController : Controller { /********* ** Properties @@ -39,7 +39,7 @@ namespace StardewModdingAPI.Web.Controllers /// Construct an instance. /// The cache in which to store mod metadata. /// The config settings for mod update checks. - public ModsController(IMemoryCache cache, IOptions configProvider) + public ModsApiController(IMemoryCache cache, IOptions configProvider) { ModUpdateCheckConfig config = configProvider.Value; diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index eaf14983..abce8f28 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -64,6 +64,7 @@ namespace StardewModdingAPI.Web loggerFactory.AddDebug(); app .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseStaticFiles() // wwwroot folder .UseMvc(); } } diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000..89b1866c --- /dev/null +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -0,0 +1,29 @@ + + + + + @ViewData["Title"] - SMAPI.io + + + + +
+
+

@ViewData["Title"]

+ @RenderBody() +
+ +
+ + diff --git a/src/SMAPI.Web/Views/_ViewStart.cshtml b/src/SMAPI.Web/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/src/SMAPI.Web/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/SMAPI.Web/wwwroot/Content/main.css b/src/SMAPI.Web/wwwroot/Content/main.css new file mode 100644 index 00000000..c8ce8d33 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/main.css @@ -0,0 +1,107 @@ +/* tags */ +html { + height: 100%; +} + +body { + height: 100%; + font-family: sans-serif; +} + +h1, h2, h3 { + font-weight: bold; + margin: 0.2em 0 0.1em 0; + padding-top: .5em; +} + +h1 { + font-size: 1.5em; + color: #888; + margin-bottom: 0; +} + +h2 { + font-size: 1.5em; + border-bottom: 1px solid #AAA; +} + +h3 { + font-size: 1.2em; + border-bottom: 1px solid #AAA; + width: 50%; +} + +a { + color: #006; +} + +/* content */ +#content-column { + position: absolute; + top: 1em; + left: 10em; +} + +#content { + min-height: 140px; + padding: 0 1em 1em 1em; + border-left: 1px solid #CCC; + background: #FFF; + font-size: 0.9em; +} + +#content p { + max-width: 55em; +} + +.section { + border: 1px solid #CCC; + padding: 0.5em; + margin-bottom: 1em; +} + +/* sidebar */ +#sidebar { + margin-top: 3em; + min-height: 75%; + width: 12em; + background: url("sidebar-bg.gif") no-repeat top right; + color: #666; +} + +#sidebar h4 { + margin: 0 0 0.2em 0; + width: 10em; + border-bottom: 1px solid #CCC; + font-size: 0.8em; + font-weight: normal; +} + +#sidebar a { + color: #77B; + border: 0; +} + +#sidebar ul, #sidebar li { + margin: 0; + padding: 0; + list-style: none none; + font-size: 0.9em; + color: #888; +} + +#sidebar li { + margin-left: 1em; +} + +/* footer */ +#footer { + margin: 1em; + padding: 1em; + font-size: 0.6em; + color: gray; +} + +#footer a { + color: #669; +} diff --git a/src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif b/src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..48e9af5a033ad34ecfe9b737d48a31d1172fe396 GIT binary patch literal 1104 zcmb8o2{+pZ008iRB8em-!5pFPTO17y646y#qa`+Brmt&N>PlLz)k-5B3klLK4cXSY zg4AA2bhWiV>LERl_WnqQpF4sSJ?$XFV2Bq1@baO8 zKy)w|KIU1`HxW1p0?)c3#wUaX*vFt7ZiaIzmN!2K1S^KFZv>#EU?hA)_#33#F*bRS z@abMi-XW=YpvG=c@AS5r&f{=ZU|Wg0kr!T!v~Pa7XL=qir0KEv*#7LHqHn@r;zrp* zKRcnRWB>)gp`4mdy6kfgwhO`#IrO^W+JXbf9J!(8NutjnJcn4G>y7(HaYkx#^)wz0nw6S5LC$B9DO5Jgqu zENe6RGx}1 z4aYUd+7s^XWQQ6Ssz?!KR|E##zDf)b{}J-2&VA)48*#Rk7_uXF#@!AGVg>5nQ*L&o zf%LPtqUNN*ZST;lCg8V;op%nAkX=0G=8E{u3jX8#r8~`tmkV;2Dh^?nZsvu2`A$1)8E>$Y21i5 z_>@q1JpudW?oN7A{^jq?fv6|c8iSlJB?9HX!GU`xYi#S~B|6Ey!x9(b| zDL@O>bU$ozs-Kh**<$dm+_fmF7C7ZjA&gm%R2n*668Ht(Tc#Pm|At1tQ;3+ErBv!4 zoNUVBSJ_<;O=TEm9(DJa5kjfY8uM=jC@v*k)o$Sc$*x3IN?1EdB0+KER;Sx~UX?ciYwesrP{B^(!wAB}W7W^_KL{EXB5(gXp8#ev_E+nN42H7BflUbG_qlE^dZQ&lQUHAXX;))pSK(Y S?U`hczy3Lf2sHr%K989*-k|*xja#CpdZ>0YtX(P(hVqgC+u~z=kdF9jpmcOmTNXzJR9^E zm*<8mxHTJ^TJ@jPM-jCfjPl9e4*k~xB5OVq+dcTD=~8&aKJo6cLyX}k#7IsTx5+2O zZ%nF`?Y>+fW0akTYe+GZNY~{vg{w(xl&(_VGLAos^UE_JT9wv+}Aqcb+DmuS}G-pQlUz zj^US#l9N2yv?wHKKgSi4r;Ih)g^1o0J%TT8t%^g>{T| z*A?gDbAKMqM)4f{Zy6+wJsNKCg=E6O+L7ZPuyXjJ3#t-7W z`KlP--W0dXx5R1l4RP6WMFRS*l3EQvRk~;U>_ES+RXZkc@E@}2RGFA{M_s?UCcd+d zNr|_=x>$M8m#GBm1Z8h$f5z|ru>XN2YxmkxBKD|Ms=rVI>h`1k*COn7bIQ}Eyn~1A zDE`zcDb?bN_*Rd&BRr>iEHey|2Laca0H6V& z*O?f#R-wT5mz(^&mUB!%G}e-mNQ*G<8WLGAuk9bSo{|?DDM-nq0>J#>2kken?IA1L zlKGd*hmHCG75tux<~TKmn(`ys77B@IWN}gBM_qp+uVcAaJ@gd`2;@`8cY~I{7xnkv ztKWP5jy||Q^{w>c%q9MBER)iWV&(CtW5l^ra*lZ9#mVbfzs!KO=>+rGo1VUxoZNm# zdp|(Rc3!OZt)RBkC8XH|a}VP_mJOx;y@KnYxUWc8U>@k3TmSZQ*U$)e=ytD0Q2oKw z;X(FM_JdVwuXBw%B5wV@%PN<95cWZ$;MyxnuG42{#C^zq#RcuD2lkb#JwRp|*U)ZZ zZndMu>*=0StZ9F#*lwa!Yo8?5T27Wy{WqF2CY%u?7UPabs@7F~(qz)UtI-ov{AGpVV(PUYtsO?9wusV%*h$!IfY8>~M{4vn8z88fiFehmv1s z$$5G5i;GgQ_t)YYv04gto@LtHt24xTz!C?%qP~!-2`8oazF)-oxHW!q*!7!|?KA0+ zVDFNeZ70V;zS?6o2aLM|RM2v*u^)A>-zVLx#aMZE$X4k%Jx!7i-;|-7F01~^j!6^GPtw%5Df{DPhh@Uf z3wNOR!oyOsL$Y`#9~5JsHU23-h(SOP1!w&ruKo84&XA|TVUgy*ar-okkO!`Dp!5N`8q1%<1?uO6RQGd)^)ywb86I`{?iLe8V%&CmS!QJ_;utzCHFN zoD&Jo1J8@Fks2)%vdlmG4D2vQ$r`u&x}==ArSe+;+WxrKurBdvi$SW+u+3K-@)N$x zP-W%U;EbkiFNZX{{h_~T%w2L&m#NZx&?@=I={-()4f_sR=uY}J!6e|mkJb^b#!seI(76hB~cD%X3J1idj=q4>m9RlnS!n?g{& z>-5v&)pHl^e;4FVvfIDi{y1i015=U8$wY2UHo4{-<+h*`UBVfD1BC@OUn=ENO)ATRC+BzLYt3~ux9aMVD1a7 z|5?bv33_ndc3GPBijj)bmaG1ij%~8}Tz6Q!)QuUV+WJSW)^nbH{gDRmO;ux#UUpvo zG3AKE&p0>O|0Jx*IMa5t{ZYoq7h^MQdZ+m==Jqpe{f1@h zS`T>da$lIH_~X55Fl6%_;jHIgr#L%5TPP%QB>}lhb-7Nhc2f4urD9C}K|E)j6z>J# zq2(Ygbn%wBUVvWKTo>1+>Ebi@xVY@gkir|&^|ErtRO`%Ie`Av9Q^J4 z5yL5q{VWW339t*=h8>aW9om|M9?JGZBVXaO;vX8R_@fUm_CYY89~*tpx{#R&2>4q* z3477@LEEwA!9UA0#-aG5Jb-*R7QU~{7pJJT_rw!z6L+(ZDzDDo z;WOvB5X`d-7@Aid=C>aDE~WKV?49*u3_~mkaTJyje%&{j??(C^9lsmH3A}5o)Xd{d zjQXP_==Hgdb35}a;~4VG%U&P;6$l#$Ctw#UEnhDt9DRqQ?{=20O0iyB#7H|K&pGlRMmy2AV&xgj-2<*!VCQez6#1Kdo3?1pMV|OW*RJ5X0lMDc z(O}?duVy2~x8)dVGGwDd`6>IvYvC#JnsY+MO&B|Ii92T61Y^ClZE{b+JGf;Jj&vsO zWErDk$}v#?dJY&vvg&&b+i&(gB-NA!dBOjh!PpS*1l2o~-OlZKtaa|@`WGRd4L=s2 zRtfS*yQwN>!`PuC4!Fa7C%)~I*>t@EccBh`7S}|?+~^C8v(s)AtlxB=S zmxd~{_ODi-ej^>Ai+Y?iK|#i93V@o7>tE=_y^+6?thL`~l*pR7H3PXu>cq~iI0aoeT95$GH-SLG+0 z5Z|U>5X@svoL5{H&)(lC1h<~LALG~sjL2#?=2^w_*^iVS8Ru>f?&PoI_>4K5HgsId zpH5Nhl)ibMA@_x+GG!6*eRd#rtI9NZ7;Ui}31{K+wQ{-#YS{_Gp~ z7AF7OK1(~w&R_FZDfD9UKQ4Fh=NZD{>ex5*cpy*n#~8BnSFsJh#ve}Ny>78)G4f>B z`3jAPZd1DIG~!}{;kXO}o7B3*V`oEDrAmzY%Xfp7bK@spGS zr_%6GJBsYg5Fgw{x$naMr*#aT^ksc7r2LxP8BdRg9LRHBd|usM7oV`_D#pP3YNCSn zWz4}~Wdn$F>NUMq-*t+A=b4so!WN!e0REOwslAl>W!7HH^Beb9g8Q#spEj;{=$HI) z_rg0EeGjARtigFnouMFOBgTSyMP?k)e!w&soI4Qib$c#yuisZZ!~03ofnO=Snz&y+ zSe`C@@GY5X2bm^+@*9D>FLjRzJGQ?2Qg>fTcqXVj>QKS7VJ|p?-_!$H;IfJ1#C_Ny z+}+^2D}s47H(Y$nJN9+ zq5sjir?BhS!Bum%IM~W7c3Z5@tB~!`A@?ZNPxij}O#M?=n1j2mpB#f;|H+@cxt6Q- zoV*oxKHK@@I|GMJnEG|-+fkl%4CHU;&b;Dpo1-j!DEqlPf35#2Zzf6W_9bkaPi-^We$@VJ zo!y$gwMgku-7{Hp&U+ru%uF=~t-truf06mP2i5nSy8QN>S$4*`LO;B-<-IAvJj?X? z$C|U;&Oho5(6D!k@NS&G$~#)3-{(i9`?ReRIc1B~>G`>dm#3KgTTa5dgKrP8_7pty ztGcV=Jea!vLz;ZCMIvU-mG&vit$DKea-Dbhgq6n_N^U)`>a$JBCFYvu6Yu!Ohca{I zZ^mXKrPIje%2)ZFL;EE2%*<~O)cjyy(&r5Q{(Jj7C)lT6KdDR2ty2&49+r2lx!Jd) zKE^Q%@B6&BRd=_zPleAiCS$$_=l=dnUK~TGxZC{AbMtW_f5y(ZuWFD#=Tq^w)&?~X zsn)@pLpDapI?Qat2wca z=WF;m>jO`8nWy>Hz?KS<5^ z73SUK&oesWpE#@B)<3w@KD5tngsoe#Q1WLV)CzxB|+GvG&rH6K5d z-|`?mVJlm}vf#Joi0AK_3Y7+aslNFMdk%H+-HAfuv52FN@_VX9{PSV0P!2F}^^@RMf=z3`)#?p{UxCFIC*4Irb31G8>CA0S@LAfslwkZX#Roh!$OYVvEDDg zytcz$jToL@gUyuxpFn>L;a_FHlrFtVN|sw}?(5t;L(Kc5yvq11>w!CCkmgy#+Dpja zzh$O51{;61udA{C{rA`4JPF9~sNOTfzw|m$sDBo`@&9RB-jved@83%BUCd|L6W=9v zqdvdaamU%^j_Wywx!CI tKdbLI?$zJJ{vAA({pd5cey8A^XjtdLKb^n%J&@r4MDg$c(}%5x{{m{< Date: Fri, 27 Oct 2017 19:36:52 -0400 Subject: [PATCH 26/59] add placeholder for new log parser (#358) --- .../Controllers/LogParserController.cs | 19 +++++++++++++++++++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 14 ++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/SMAPI.Web/Controllers/LogParserController.cs create mode 100644 src/SMAPI.Web/Views/LogParser/Index.cshtml diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs new file mode 100644 index 00000000..4ed8898a --- /dev/null +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides a web UI and API for parsing SMAPI log files. + [Route("log")] + internal class LogParserController : Controller + { + /********* + ** Public methods + *********/ + /// Render the web UI to upload a log file. + [HttpGet] + public ViewResult Index() + { + return this.View("Index"); + } + } +} diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml new file mode 100644 index 00000000..cd47d687 --- /dev/null +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -0,0 +1,14 @@ +@{ + ViewData["Title"] = "SMAPI log parser"; +} + +

How to share a SMAPI log

+
    +
  1. Find your SMAPI log.
  2. +
  3. Click the file and drag it onto the upload form below (or paste the text in).
  4. +
  5. Click Parse.
  6. +
  7. Share the link you get back.
  8. +
+ +

Parsed log

+TODO From a26220e3410aa7f0a043c1bcd0ab845e210c1bbc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:37:26 -0400 Subject: [PATCH 27/59] add log parser prototype by Entoarox (#358) --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 769 +++++++++++++++++++++ 1 file changed, 769 insertions(+) diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index cd47d687..830cfe47 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -12,3 +12,772 @@

Parsed log

TODO + + + + + + + SMAPI log parser + + + + +
    +
  • TRACE
  • +
  • DEBUG
  • +
  • INFO
  • +
  • ALERT
  • +
  • WARN
  • +
  • ERROR
  • +
  • Click tabs to toggle message visibility
  • +
  • UPLOAD
  • +
+
+ + + + + + + +
+ + + + + From 6cbe43a233eccbc6c8d1cfdd9c80e391463eb7c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:37:49 -0400 Subject: [PATCH 28/59] use CDN for jQuery and lz-string (#358) --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 830cfe47..417fe428 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -486,9 +486,8 @@
- + + - From 9f5af37391ac196fe183122f57496846843335cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:38:13 -0400 Subject: [PATCH 29/59] move log parser CSS/JS out of HTML (#358) --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 674 +----------------- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 3 +- .../wwwroot/Content/css/log-parser.css | 374 ++++++++++ .../wwwroot/Content/{ => css}/main.css | 2 +- .../Content/{ => images}/sidebar-bg.gif | Bin .../wwwroot/Content/js/log-parser.js | 287 ++++++++ 6 files changed, 670 insertions(+), 670 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/Content/css/log-parser.css rename src/SMAPI.Web/wwwroot/Content/{ => css}/main.css (95%) rename src/SMAPI.Web/wwwroot/Content/{ => images}/sidebar-bg.gif (100%) create mode 100644 src/SMAPI.Web/wwwroot/Content/js/log-parser.js diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 417fe428..021293b6 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,6 +1,12 @@ @{ ViewData["Title"] = "SMAPI log parser"; } +@section Head { + + + + +}

How to share a SMAPI log

    @@ -14,387 +20,10 @@ TODO - SMAPI log parser - @@ -486,296 +115,5 @@
    - - - diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 89b1866c..547a8178 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -3,7 +3,8 @@ @ViewData["Title"] - SMAPI.io - + + @RenderSection("Head", required: false) '); + } + else { + $("#input").val(LZString.decompressFromUTF16(data) || data); + loadData(); + } + $("#uploader").fadeOut(); + }); + } + var Stage, + flags = $("#modflags"), + output = $("#output"), + filters = 0, + memory = "", + versionInfo, + modInfo, + modMap, + modErrors, + logInfo, + templateBody = $("#template-body").text(), + templateModentry = $("#template-modentry").text(), + templateCss = $("#template-css").text(), + templateLogentry = $("#template-logentry").text(), + templateLognotice = $("#template-lognotice").text(), + regexInfo = /\[[\d\:]+ INFO SMAPI] SMAPI (.*?) with Stardew Valley (.*?) on (.*?)\n/g, + regexMods = /\[[^\]]+\] Loaded \d+ mods:(?:\n\[[^\]]+\] .+)+/g, + regexLog = /\[([\d\:]+) (TRACE|DEBUG|INFO|WARN|ALERT|ERROR) ? ([^\]]+)\] ?((?:\n|.)*?)(?=(?:\[\d\d:|$))/g, + regexMod = /\[(?:.*?)\] *(.*?) (\d+\.?(?:.*?))(?: by (.*?))? \|(?:.*?)$/gm, + regexDate = /\[\d{2}:\d{2}:\d{2} TRACE SMAPI\] Log started at (.*?) UTC/g, + regexPath = /\[\d{2}:\d{2}:\d{2} DEBUG SMAPI\] Mods go here: (.*?)(?:\n|$)/g + ; + $("#tabs li:not(.notice)").on("click", function(evt) { + var t = $(evt.currentTarget) + t.toggleClass("active"); + $("#output").toggleClass(t.text().toLowerCase()); + }) + $("#upload").on("click", function() { + memory = $("#input").val() || ""; + $("#input").val(""); + $("#popup-upload").fadeIn(); + }) + var proxies = [ + "https://cors-anywhere.herokuapp.com/", + "https://galvanize-cors-proxy.herokuapp.com/" + ]; + $('#popup-upload').on({ + 'dragover dragenter': function(e) { + e.preventDefault(); + e.stopPropagation(); + }, + 'drop': function(e) { + $("#uploader").attr("data-text", "Reading...") + $("#uploader").show(); + var dataTransfer = e.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.files.length) { + e.preventDefault(); + e.stopPropagation(); + var file = dataTransfer.files[0]; + var reader = new FileReader(); + reader.onload = $.proxy(function(file, $input, event) { + $input.val(event.target.result); + $("#uploader").fadeOut(); + $("#submit").click(); + }, this, file, $("#input")); + reader.readAsText(file); + } + } + }); + function logSize(id, str) { + console.log(id + ":", str.length * 2, "bytes", Math.round(str.length / 5.12) / 100, "kb"); + } + $("#submit").on("click", function() { + $("#popup-upload").fadeOut(); + if ($("#input").val()) { + memory = ""; + var raw = $("#input").val(); + var paste = LZString.compressToUTF16(raw); + logSize("Raw", raw); + logSize("Compressed", paste); + if (paste.length * 2 > 524288) { + $("#output").html('

    Unable to save!

    This log cannot be saved due to its size.
    ' + $("#input").val() + '
    '); + return; + } + console.log("paste:", paste); + var packet = { + api_dev_key: "b8219d942109d1e60ebb14fbb45f06f9", + api_option: "paste", + api_paste_private: 1, + api_paste_code: paste, + api_paste_expire_date: "1W" + }; + $("#uploader").attr("data-text", "Saving..."); + $("#uploader").fadeIn(); + var uri = proxies[Math.floor(Math.random() * proxies.length)] + "pastebin.com/api/api_post.php"; + console.log(packet, uri); + $.post(uri, packet, function(data, state, xhr) { + $("#uploader").fadeOut(); + console.log("Result: ", data); + if (data.substring(0, 15) == "Bad API request") + $("#output").html('

    Parsing failed!

    Parsing of the log failed, details follow.
     

    Stage: Upload

    Error: ' + data + '
    ' + $("#input").val() + '
    '); + else if (data) + location.href = "?" + data.split('/').pop(); + else + $("#output").html('

    Parsing failed!

    Parsing of the log failed, details follow.
     

    Stage: Upload

    Error: Received null response
    ' + $("#input").val() + '
    '); + }) + } else { + alert("Unable to parse log, the input is empty!"); + $("#uploader").fadeOut(); + } + }) + $("#cancel").on("click", function() { + $("#popup-upload").fadeOut(400, function() { + $("#input").val(memory); + memory = ""; + }); + }); + $("#closeraw").on("click", function() { + $("#popup-raw").fadeOut(400); + }) + if (location.search) { + getData(); + } + else + $("#popup-upload").fadeIn(); +}) From 467b9aa2df8532aa3cb94c84307c7012573d61d4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:38:37 -0400 Subject: [PATCH 30/59] integrate prototype into page layout (#358) --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 197 +++++++++--------- .../wwwroot/Content/css/log-parser.css | 77 +++---- .../wwwroot/Content/js/log-parser.js | 2 +- 3 files changed, 122 insertions(+), 154 deletions(-) diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 021293b6..87a3962b 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -6,114 +6,107 @@ + } -

    How to share a SMAPI log

    -
      -
    1. Find your SMAPI log.
    2. -
    3. Click the file and drag it onto the upload form below (or paste the text in).
    4. -
    5. Click Parse.
    6. -
    7. Share the link you get back.
    8. -
    +@********* +** Intro +*********@ +

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

    +

    Parsed log

    -TODO - - - - - SMAPI log parser - - - -
      -
    • TRACE
    • -
    • DEBUG
    • -
    • INFO
    • -
    • ALERT
    • -
    • WARN
    • -
    • ERROR
    • -
    • Click tabs to toggle message visibility
    • -
    • UPLOAD
    • -
    -
    - - - - - -