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 99e771cef..fc56adc8e 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 03cd26c98..73ecd56ea 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 @@ public void Serialisable(string versionStr) [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 48159f61a..1884afe9f 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 @@ internal class GameVersion : SemanticVersion ["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 3193aa3c0..77b93b66d 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 @@ internal class JsonHelper 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 37108556d..e825c8807 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 77b93b66d..d923ec0cf 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -19,9 +19,9 @@ internal class JsonHelper 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 e825c8807..7afe86cdf 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 b8d5990e4..6f7c2b3fa 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 fc56adc8e..0471874cd 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 66cb19f22..617dac356 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -49,7 +49,7 @@ public void SuppressButton(SButton button) { // 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 0471874cd..285d9df38 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 5c2d90faa..23a485050 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -38,7 +38,7 @@ public IPrivateField GetPrivateField(object obj, string name, bo 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 @@ public IPrivateField GetPrivateField(object obj, string name, bo 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 @@ public IPrivateProperty GetPrivateProperty(object obj, string na 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 @@ public IPrivateProperty GetPrivateProperty(object obj, string na 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 @@ public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = 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 @@ public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = 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 @@ public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentT 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 @@ public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentT 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 285d9df38..e4b2bccda 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 db2025678..2f5d104fe 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -510,16 +510,12 @@ private IEnumerable> GetInterceptors(IDictionar { 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 e4b2bccda..199e32c56 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 8d4354162..8788b142e 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 @@ public ReflectionHelper(string modID, string modName, Reflector reflector) /// 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 @@ public IPrivateField GetPrivateField(object obj, string name, bo /// 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 @@ public IPrivateField GetPrivateField(Type type, string name, boo /// 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 @@ public IPrivateProperty GetPrivateProperty(object obj, string na /// 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 @@ public IPrivateProperty GetPrivateProperty(Type type, string nam /// 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 @@ public TValue GetPrivateValue(object obj, string name, bool required = t /// 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 @@ public TValue GetPrivateValue(Type type, string name, bool required = tr /// 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 @@ public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = /// 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 @@ public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = t /// 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 @@ public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentT /// 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 199e32c56..655369155 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 0ec799dbe..bd6635c70 100644 --- a/src/SMAPI/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -615,6 +615,18 @@ internal static SButton ToSButton(this Buttons key) 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 655369155..452fd40af 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 617dac356..ff9046759 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -2,7 +2,6 @@ using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Events @@ -20,7 +19,14 @@ public class EventArgsInput : EventArgs 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 @@ public class EventArgsInput : EventArgs /// 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 c31eb698c..985aed99e 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 @@ public static class InputEvents /// 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 6f8f7cef4..ca19d726e 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -12,7 +12,6 @@ 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 @@ protected override void Update(GameTime gameTime) 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 @@ protected override void Update(GameTime gameTime) // 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 @@ protected override void Update(GameTime gameTime) // 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 000000000..10c41d086 --- /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 2f5d104fe..0b6daaa6d 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 @@ 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 @@ internal class SContentManager : LocalizedContentManager /// 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 @@ internal class SContentManager : LocalizedContentManager 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 @@ internal class SContentManager : LocalizedContentManager /// 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 @@ public SContentManager(IServiceProvider serviceProvider, string rootDirectory, C /// 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 @@ public override T Load(string assetName) /// 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 @@ public T LoadFor(string assetName, ContentManager instance) this.Cache[assetName] = data; this.TrackAssetLoader(assetName, instance); return data; - } + }); } /// Inject an asset into the cache. @@ -195,12 +178,12 @@ public T LoadFor(string assetName, ContentManager instance) /// 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 @@ public string GetLocale() /// 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 @@ public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) // 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 @@ public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) /// 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 @@ internal void DisposeFor(ContentManagerShim shim) { this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - 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 @@ private bool IsNormalisedKeyLoaded(string normalisedAssetName) /// 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 @@ private IDictionary GetKeyLocales(Reflector reflection) 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 @@ private void ParseCacheKey(string cacheKey, out string assetKey, out string loca 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 @@ private void ParseCacheKey(string cacheKey, out string assetKey, out string loca } // handle simple key - assetKey = cacheKey; + assetName = cacheKey; localeCode = null; } @@ -519,12 +510,51 @@ private IEnumerable> GetInterceptors(IDictionar } } - /// 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 ca19d726e..c62c13936 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -179,7 +179,7 @@ internal SGame(IMonitor monitor, Reflector reflection) // 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 6f7c2b3fa..605292b29 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 c62c13936..c886a4b77 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -240,6 +240,9 @@ protected override void Update(GameTime gameTime) 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 @@ protected override void Update(GameTime gameTime) 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 eb27ac528..a03ad6a42 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 08204b7ec..8a75d9255 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -19,6 +19,9 @@ internal class PrivateProperty : IPrivateProperty /// 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 @@ internal class PrivateProperty : IPrivateProperty /// 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 @@ public TValue GetValue() { 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 @@ public void SetValue(TValue value) { 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 8a75d9255..718594eeb 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -51,8 +51,10 @@ public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool 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 718594eeb..0fa106012 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -19,8 +19,8 @@ internal class PrivateProperty : IPrivateProperty /// 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 @@ public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool 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 @@ public TValue GetValue() { 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 @@ public void SetValue(TValue value) { 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 0fa106012..be346d717 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 @@ internal class PrivateProperty : IPrivateProperty /********* ** 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 @@ internal class PrivateProperty : IPrivateProperty /// 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 @@ public TValue GetValue() 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 @@ public void SetValue(TValue value) 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 | 189 ++------- src/SMAPI/Framework/SContentManager.cs | 382 ++++++++++++++---- src/SMAPI/IContentHelper.cs | 3 +- 3 files changed, 338 insertions(+), 236 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4f5bd2f08..2dd8a2e3e 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 @@ public ContentHelper(SContentManager contentManager, string modFolderPath, strin 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 @@ public T Load(string key, ContentSource source = ContentSource.ModFolder) { 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 @@ public T Load(string key, ContentSource source = ContentSource.ModFolder) 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; - } - - // 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); - - // 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'."); + // 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); + + // 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 @@ private void FixLocalMapTilesheets(Map map, string mapKey) 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 @@ private void FixLocalMapTilesheets(Map map, string mapKey) } } - /// 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 @@ private string TryLoadTilesheetImageSource(string relativeMapFolder, string imag 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 @@ private string GetModAssetPath(string localPath, string absolutePath) 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 0b6daaa6d..10d854d91 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 @@ internal class SContentManager : LocalizedContentManager /// 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 @@ internal class SContentManager : LocalizedContentManager /********* ** 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 @@ public SContentManager(IServiceProvider serviceProvider, string rootDirectory, C 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 @@ public string NormaliseAssetName(string assetName) return this.Cache.NormaliseKey(assetName); } + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public void AssertValidAssetKeyFormat(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + } + + /// 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,86 +172,105 @@ public bool IsLoaded(string assetName) 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. + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + return this.WithReadLock(() => + this.Cache.Keys + .Select(this.GetAssetName) + .Distinct() + ); + } + + /// Load an asset through the content pipeline. When loading a .png file, this must be called outside the game's draw loop. + /// The expected asset type. + /// The asset path relative to the content directory. 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. + /// 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); - return this.WithWriteLock(() => + + // 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 { - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) + return this.WithWriteLock(() => { - this.TrackAssetLoader(assetName, instance); - return base.Load(assetName); - } + // try cache + if (this.IsLoaded(assetName)) + return this.LoadImpl(assetName, instance); - // 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; - }); - } + // get file + FileInfo file = this.GetModFile(assetName); + if (!file.Exists) + throw GetContentError("the specified path doesn't exist."); - // update cache & return data - this.Cache[assetName] = data; - this.TrackAssetLoader(assetName, instance); - return data; - }); + // 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. - 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() + /// The content manager instance for which to load the asset. + public void Inject(string assetName, T value, ContentManager instance) { - return this.WithReadLock(() => - this.Cache.Keys - .Select(this.GetAssetName) - .Distinct() - ); + 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 @@ public bool InvalidateCache(Func predicate, bool dispose = f }); } + /**** + ** 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 @@ internal void DisposeFor(ContentManagerShim shim) /********* ** 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 @@ protected override void Dispose(bool disposing) 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 @@ private void ParseCacheKey(string cacheKey, out string assetName, out string loc 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 @@ private IEnumerable> GetInterceptors(IDictionar } } + /// 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 b78b165b4..7900809f2 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewValley; +using xTile; namespace StardewModdingAPI { @@ -29,7 +30,7 @@ public interface IContentHelper : IModLinked ** 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 10d854d91..54ebba833 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -531,7 +531,7 @@ private FileInfo GetModFile(string path) // 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 452fd40af..a06fc0c4a 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 2dd8a2e3e..ae812e718 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -117,7 +117,7 @@ public T Load(string key, ContentSource source = ContentSource.ModFolder) // 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 @@ public bool InvalidateCache() /********* ** 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 @@ private void FixLocalMapTilesheets(Map map, string mapKey) { 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 @@ private void FixLocalMapTilesheets(Map map, string mapKey) /// 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 @@ private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSo 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 a06fc0c4a..ba0815b3c 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 fe306e241..ce547d9b4 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -6,6 +6,7 @@ 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 @@ internal class Program : IDisposable /// 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 @@ private void HandleCommand(string name, string[] arguments) /// 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 ba0815b3c..9366e1fcf 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 b223aa9f0..54d5e47b2 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 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg { 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 ee138524a..aa11344e0 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 9366e1fcf..1202407f1 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 1a132e549..cbc8a4014 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -97,6 +97,7 @@ private IEnumerable GetUninstallPaths(DirectoryInfo installDir, Director // 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 5db02cd67..966589285 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 @@ private void GameEvents_UpdateTick(object sender, EventArgs e) } } - /// 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 6bcd3ff89..3ad1e1686 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 3d97e7996..a0b739f81 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 8c6e9f3b9..e4010111d 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 367a70c69..54d27185c 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 67fa83a37..0257892f3 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 478402021..81167747e 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 5f14edbbd..34f1760c0 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 7f4f454ce..942a50b83 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 28ace0df5..5d0985937 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 f64e9035b..2e8f6630f 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 59b28a3c5..9c66c4feb 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 54d5e47b2..688912671 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 4b9d87dc0..f4ae06945 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 3997bb1b9..5bce5ea36 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 55e069a4a..3fc504b17 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 3fd4475c8..5b1225e8a 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 40b87b623..e9693540c 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 d44d1370d..866c3d22d 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 96e34af2b..b59be2e56 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 abe9ee418..466b8f6ef 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 4e62cf775..da1170064 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 13d083981..2627b7140 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 54267384a..8d6bd7594 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 225ec0918..1024b7b60 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 96c3d9204..897d052f2 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 c827ea5ee..d6c71387e 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 760fc170e..66abd6dce 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 423455e91..797d46500 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 146da1a80..3eede4130 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 96d3159ee..b5fe9f2f3 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 000000000..ac15ec72d --- /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 cb5ec47e2..437d09868 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 22e35bce0..664dfabfc 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 89a8d45c4..8d730f372 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 0b19e78a4..000000000 --- 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 437d09868..f228bb25a 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 a671ddca5..1db5b59e5 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 @@ internal class ModsController : Controller /// 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 eaf149836..abce8f28c 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -64,6 +64,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF 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 000000000..89b1866c5 --- /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 000000000..a5f10045d --- /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 000000000..c8ce8d33b --- /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 000000000..4ed8898ab --- /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 000000000..cd47d6879 --- /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 cd47d6879..830cfe47d 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 830cfe47d..417fe428e 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 417fe428e..021293b67 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 89b1866c5..547a8178d 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 021293b67..87a3962b4 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
    • -
    -
    - - - - - -