From 2ff937397163f0ad5940b636bc7312ac747d9c39 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 10:59:57 -0400 Subject: fix compatibility check crashing for players with SDV 1.08 --- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 3 ++- src/SMAPI/Framework/GameVersion.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 03cd26c9..73ecd56e 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using NUnit.Framework; @@ -239,6 +239,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("1.06")] [TestCase("1.07")] [TestCase("1.07a")] + [TestCase("1.08")] [TestCase("1.1")] [TestCase("1.11")] [TestCase("1.2")] diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 48159f61..1884afe9 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace StardewModdingAPI.Framework @@ -22,6 +22,7 @@ namespace StardewModdingAPI.Framework ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", ["1.07a"] = "1.0.8-prerelease1", + ["1.08"] = "1.0.8", ["1.11"] = "1.1.1" }; -- cgit From 51a2c3991f3c76197afb21a42a30f2a91a7f9908 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 16:47:32 -0400 Subject: simplify SelectiveStringEnumConverter implementation --- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 7 +++--- .../Serialisation/SelectiveStringEnumConverter.cs | 25 +++++----------------- 2 files changed, 9 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 3193aa3c..77b93b66 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; -using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.Serialisation { @@ -20,7 +19,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) + new SelectiveStringEnumConverter(), + new SelectiveStringEnumConverter(), + new SelectiveStringEnumConverter() } }; diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs index 37108556..e825c880 100644 --- a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs @@ -1,37 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System; using Newtonsoft.Json.Converters; namespace StardewModdingAPI.Framework.Serialisation { - /// A variant of which only converts certain enums. - internal class SelectiveStringEnumConverter : StringEnumConverter + /// A variant of which only converts a specified enum. + /// The enum type. + internal class SelectiveStringEnumConverter : StringEnumConverter { - /********* - ** Properties - *********/ - /// The enum type names to convert. - private readonly HashSet Types; - - /********* ** Public methods *********/ - /// Construct an instance. - /// The enum types to convert. - public SelectiveStringEnumConverter(params Type[] types) - { - this.Types = new HashSet(types.Select(p => p.FullName)); - } - /// Get whether this instance can convert the specified object type. /// The object type. public override bool CanConvert(Type type) { return base.CanConvert(type) - && this.Types.Contains((Nullable.GetUnderlyingType(type) ?? type).FullName); + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); } } } -- cgit From a4fb2331fe57102aa8e8b30efb8095a1edb6b923 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 16:58:42 -0400 Subject: simplify JSON converter name --- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 6 +++--- .../Serialisation/SelectiveStringEnumConverter.cs | 22 ---------------------- .../Framework/Serialisation/StringEnumConverter.cs | 22 ++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) delete mode 100644 src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs create mode 100644 src/SMAPI/Framework/Serialisation/StringEnumConverter.cs (limited to 'src') diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 77b93b66..d923ec0c 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -19,9 +19,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(), - new SelectiveStringEnumConverter(), - new SelectiveStringEnumConverter() + new StringEnumConverter(), + new StringEnumConverter(), + new StringEnumConverter() } }; diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs deleted file mode 100644 index e825c880..00000000 --- a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Newtonsoft.Json.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// A variant of which only converts a specified enum. - /// The enum type. - internal class SelectiveStringEnumConverter : StringEnumConverter - { - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type type) - { - return - base.CanConvert(type) - && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs new file mode 100644 index 00000000..7afe86cd --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// A variant of which only converts a specified enum. + /// The enum type. + internal class StringEnumConverter : StringEnumConverter + { + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type type) + { + return + base.CanConvert(type) + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index b8d5990e..6f7c2b3f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -173,7 +173,7 @@ - + -- cgit From 36b4e550f1945ef710fca2c6deab7df94e708ef7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Oct 2017 21:26:00 -0400 Subject: 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(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index fc56adc8..0471874c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,9 @@ * For players: * Fixed compatibility check crashing for players with Stardew Valley 1.08. +* For modders: + * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. + ## 2.0 ### Release highlights * **Mod update checks** diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 66cb19f2..617dac35 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Events { // keyboard if (this.Button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Except(new[] { key }).ToArray()); + Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); // controller else if (this.Button.TryGetController(out Buttons controllerButton)) -- cgit From 53df85f3123f8d9cb00013bb32b61c220ccad697 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 20 Oct 2017 16:37:22 -0400 Subject: 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(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0471874c..285d9df3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ * Fixed compatibility check crashing for players with Stardew Valley 1.08. * For modders: + * The reflection API now works with public code to simplify mod integrations. * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. ## 2.0 diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 5c2d90fa..23a48505 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && field == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); return field; @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateField GetPrivateField(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); if (required && field == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); return field; @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && property == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); return property; @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && property == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); return property; @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); return method; @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); return method; @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); return method; @@ -155,7 +155,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); return method; -- cgit From 85a8959e97e90b30ac8291904838e18f102e97c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Oct 2017 21:51:48 -0400 Subject: 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(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 285d9df3..e4b2bccd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For modders: * The reflection API now works with public code to simplify mod integrations. * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. + * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. ## 2.0 ### Release highlights diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index db202567..2f5d104f 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -510,16 +510,12 @@ namespace StardewModdingAPI.Framework { foreach (var entry in entries) { - IModMetadata metadata = entry.Key; + IModMetadata mod = entry.Key; IList interceptors = entry.Value; - // special case if mod is an interceptor - if (metadata.Mod is T modAsInterceptor) - yield return new KeyValuePair(metadata, modAsInterceptor); - // registered editors foreach (T interceptor in interceptors) - yield return new KeyValuePair(metadata, interceptor); + yield return new KeyValuePair(mod, interceptor); } } -- cgit From f74321addc79a5616cc0f43e4f5f4b8154fac827 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 13:13:14 -0400 Subject: fix SMAPI blocking reflection access to vanilla members on overridden types (#371) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 99 ++++++++++++++-------- 2 files changed, 67 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e4b2bccd..199e32c5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * The reflection API now works with public code to simplify mod integrations. * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. + * Fixed SMAPI blocking reflection access to vanilla members on overridden types. ## 2.0 ### Release highlights diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 8d435416..8788b142 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -42,8 +43,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Returns the field wrapper, or null if the field doesn't exist and is false. public IPrivateField GetPrivateField(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateField(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateField(obj, name, required) + ); } /// Get a private static field. @@ -53,8 +55,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateField GetPrivateField(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateField(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateField(type, name, required) + ); } /**** @@ -67,8 +70,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateProperty(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateProperty(obj, name, required) + ); } /// Get a private static property. @@ -78,8 +82,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateProperty(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateProperty(type, name, required) + ); } /**** @@ -98,7 +103,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); IPrivateField field = this.GetPrivateField(obj, name, required); return field != null ? field.GetValue() @@ -117,7 +121,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); IPrivateField field = this.GetPrivateField(type, name, required); return field != null ? field.GetValue() @@ -133,8 +136,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(obj, name, required) + ); } /// Get a private static method. @@ -143,8 +147,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(type, name, required) + ); } /**** @@ -157,8 +162,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required) + ); } /// Get a private static method. @@ -168,33 +174,60 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(type, name, argumentTypes, required) + ); } /********* ** Private methods *********/ - /// Assert that mods can use the reflection helper to access the given type. - /// The type being accessed. - private void AssertAccessAllowed(Type type) + /// Assert that mods can use the reflection helper to access the given member. + /// The field value type. + /// The field being accessed. + /// Returns the same field instance for convenience. + private IPrivateField AssertAccessAllowed(IPrivateField field) { - // validate type namespace - if (type.Namespace != null) - { - string rootSmapiNamespace = typeof(Program).Namespace; - if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) - throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); - } + this.AssertAccessAllowed(field?.FieldInfo); + return field; } - /// Assert that mods can use the reflection helper to access the given type. - /// The object being accessed. - private void AssertAccessAllowed(object obj) + /// Assert that mods can use the reflection helper to access the given member. + /// The property value type. + /// The property being accessed. + /// Returns the same property instance for convenience. + private IPrivateProperty AssertAccessAllowed(IPrivateProperty property) { - if (obj != null) - this.AssertAccessAllowed(obj.GetType()); + this.AssertAccessAllowed(property?.PropertyInfo); + return property; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The method being accessed. + /// Returns the same method instance for convenience. + private IPrivateMethod AssertAccessAllowed(IPrivateMethod method) + { + this.AssertAccessAllowed(method?.MethodInfo); + return method; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The member being accessed. + private void AssertAccessAllowed(MemberInfo member) + { + if (member == null) + return; + + // get type which defines the member + Type declaringType = member.DeclaringType; + if (declaringType == null) + throw new InvalidOperationException($"Can't validate access to {member.MemberType} {member.Name} because it has no declaring type."); // should never happen + + // validate access + string rootNamespace = typeof(Program).Namespace; + if (declaringType.Namespace == rootNamespace || declaringType.Namespace?.StartsWith(rootNamespace + ".") == true) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning. (Detected access to {declaringType.FullName}.{member.Name}.)"); } } } -- cgit From 99c8dd79406f5099194d72e26085a49939705259 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 15:07:06 -0400 Subject: add InputButton.ToSButton() extension --- docs/release-notes.md | 6 ++++-- src/SMAPI/SButton.cs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 199e32c5..65536915 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,8 +4,10 @@ * Fixed compatibility check crashing for players with Stardew Valley 1.08. * For modders: - * The reflection API now works with public code to simplify mod integrations. - * Fixed `e.SuppressButton()` in input events not correctly suppressing keyboard buttons. + * Added support for public code in reflection API, to simplify mod integrations. + * Improved input events: + * Added `ToSButton()` extension for the game's `Game1.options` button type. + * Fixed `e.SuppressButton()` not correctly suppressing keyboard buttons. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. * Fixed SMAPI blocking reflection access to vanilla members on overridden types. diff --git a/src/SMAPI/SButton.cs b/src/SMAPI/SButton.cs index 0ec799db..bd6635c7 100644 --- a/src/SMAPI/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -615,6 +615,18 @@ namespace StardewModdingAPI return (SButton)(SButtonExtensions.ControllerOffset + key); } + /// Get the equivalent for the given button. + /// The Stardew Valley button to convert. + internal static SButton ToSButton(this InputButton input) + { + // derived from InputButton constructors + if (input.mouseLeft) + return SButton.MouseLeft; + if (input.mouseRight) + return SButton.MouseRight; + return input.key.ToSButton(); + } + /// Get the equivalent for the given button. /// The button to convert. /// The keyboard equivalent. -- cgit From ed56cb714d7fb76f3c1b9d2f2e7b7627f8accc70 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 15:09:36 -0400 Subject: 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(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 65536915..452fd40a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,8 +6,11 @@ * For modders: * Added support for public code in reflection API, to simplify mod integrations. * Improved input events: + * Added `e.IsActionButton` and `e.IsUseToolButton`. * Added `ToSButton()` extension for the game's `Game1.options` button type. + * Deprecated `e.IsClick`, which is limited and unclear. Use `IsActionButton` or `IsUseToolButton` instead. * Fixed `e.SuppressButton()` not correctly suppressing keyboard buttons. + * Fixed `e.IsClick` (now `e.IsActionButton`) ignoring custom key bindings. * Fixed mods which implement `IAssetLoader` directly not being allowed to load files due to incorrect conflict detection. * Fixed SMAPI blocking reflection access to vanilla members on overridden types. diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 617dac35..ff904675 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Events @@ -20,7 +19,14 @@ namespace StardewModdingAPI.Events public ICursorPosition Cursor { get; set; } /// Whether the input is considered a 'click' by the game for enabling action. - public bool IsClick { get; } + [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 + public bool IsClick => this.IsActionButton; + + /// Whether the input should trigger actions on the affected tile. + public bool IsActionButton { get; } + + /// Whether the input should use tools on the affected tile. + public bool IsUseToolButton { get; } /********* @@ -29,12 +35,14 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { this.Button = button; this.Cursor = cursor; - this.IsClick = isClick; + this.IsActionButton = isActionButton; + this.IsUseToolButton = isUseToolButton; } /// Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event. diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index c31eb698..985aed99 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -1,6 +1,5 @@ using System; using StardewModdingAPI.Framework; -using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Events { @@ -24,20 +23,22 @@ namespace StardewModdingAPI.Events /// Encapsulates monitoring and logging. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); } /// Raise a event. /// Encapsulates monitoring and logging. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6f8f7cef..ca19d726 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -12,7 +12,6 @@ using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -371,7 +370,8 @@ namespace StardewModdingAPI.Framework SButton[] previousPressedKeys = this.PreviousPressedButtons; SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); - bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); + bool isUseToolButton = Game1.options.useToolButton.Any(p => framePressedKeys.Contains(p.ToSButton())); + bool isActionButton = !isUseToolButton && Game1.options.actionButton.Any(p => framePressedKeys.Contains(p.ToSButton())); // get cursor position ICursorPosition cursor; @@ -388,7 +388,7 @@ namespace StardewModdingAPI.Framework // raise button pressed foreach (SButton button in framePressedKeys) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isActionButton, isUseToolButton); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -408,10 +408,9 @@ namespace StardewModdingAPI.Framework // raise button released foreach (SButton button in frameReleasedKeys) { - bool wasClick = - (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click - || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); + bool wasUseToolButton = (from opt in Game1.options.useToolButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any(); + bool wasActionButton = !wasUseToolButton && (from opt in Game1.options.actionButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any(); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasActionButton, wasUseToolButton); // legacy events if (button.TryGetKeyboard(out Keys key)) -- cgit From 8c97a63a82729efe56d73928e9afb436dbffea56 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 23 Oct 2017 03:24:53 -0400 Subject: 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 (limited to 'src') diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs new file mode 100644 index 00000000..10c41d08 --- /dev/null +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.Content +{ + /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localisation, loading content, etc. It assumes all keys passed in are already normalised. + internal class ContentCache + { + /********* + ** Properties + *********/ + /// The underlying asset cache. + private readonly IDictionary Cache; + + /// The possible directory separator characters in an asset key. + private readonly char[] PossiblePathSeparators; + + /// The preferred directory separator chaeacter in an asset key. + private readonly string PreferredPathSeparator; + + /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. + private readonly Func NormaliseAssetNameForPlatform; + + + /********* + ** Accessors + *********/ + /// Get or set the value of a raw cache entry. + /// The cache key. + public object this[string key] + { + get => this.Cache[key]; + set => this.Cache[key] = value; + } + + /// The current cache keys. + public IEnumerable Keys => this.Cache.Keys; + + + /********* + ** Public methods + *********/ + /**** + ** Constructor + ****/ + /// Construct an instance. + /// The underlying content manager whose cache to manage. + /// Simplifies access to private game code. + /// The possible directory separator characters in an asset key. + /// The preferred directory separator chaeacter in an asset key. + public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) + { + // init + this.Cache = reflection.GetPrivateField>(contentManager, "loadedAssets").GetValue(); + this.PossiblePathSeparators = possiblePathSeparators; + this.PreferredPathSeparator = preferredPathSeparator; + + // get key normalisation logic + if (Constants.TargetPlatform == Platform.Windows) + { + IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); + this.NormaliseAssetNameForPlatform = path => method.Invoke(path); + } + else + this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + } + + /**** + ** Fetch + ****/ + /// Get whether the cache contains a given key. + /// The cache key. + public bool ContainsKey(string key) + { + return this.Cache.ContainsKey(key); + } + + + /**** + ** Normalise + ****/ + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + string normalised = string.Join(this.PreferredPathSeparator, parts); + if (path.StartsWith(this.PreferredPathSeparator)) + normalised = this.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Normalise a cache key so it's consistent with the underlying cache. + /// The asset key. + [Pure] + public string NormaliseKey(string key) + { + key = this.NormalisePathSeparators(key); + return key.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase) + ? key.Substring(0, key.Length - 4) + : this.NormaliseAssetNameForPlatform(key); + } + + /**** + ** Remove + ****/ + /// Remove an asset with the given key. + /// The cache key. + /// Whether to dispose the entry value, if applicable. + /// Returns the removed key (if any). + public bool Remove(string key, bool dispose) + { + // get entry + if (!this.Cache.TryGetValue(key, out object value)) + return false; + + // dispose & remove entry + if (dispose && value is IDisposable disposable) + disposable.Dispose(); + + return this.Cache.Remove(key); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the removed keys (if any). + public IEnumerable Remove(Func predicate, bool dispose = false) + { + List removed = new List(); + foreach (string key in this.Cache.Keys.ToArray()) + { + Type type = this.Cache[key].GetType(); + if (predicate(key, type)) + { + this.Remove(key, dispose); + removed.Add(key); + } + } + return removed; + } + } +} diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 2f5d104f..0b6daaa6 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.Xna.Framework; +using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; @@ -15,7 +15,17 @@ using StardewValley; namespace StardewModdingAPI.Framework { - /// SMAPI's implementation of the game's content manager which lets it raise content events. + /// A thread-safe content manager which intercepts assets being loaded to let SMAPI mods inject or edit them. + /// + /// This is the centralised content manager which manages all game assets. The game and mods don't use this class + /// directly; instead they use one of several instances, which proxy requests to + /// this class. That ensures that when the game disposes one content manager, the others can continue unaffected. + /// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously. + /// + /// Note that assets in the cache have two identifiers: the asset name (like "bundles") and key (like "bundles.pt-BR"). + /// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset + /// keys, and the game and mods only know about asset names. The content manager handles resolving them. + /// internal class SContentManager : LocalizedContentManager { /********* @@ -27,11 +37,8 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// The underlying content manager's asset cache. - private readonly IDictionary Cache; - - /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. - private readonly Func NormaliseAssetNameForPlatform; + /// The underlying asset cache. + private readonly ContentCache Cache; /// The private method which generates the locale portion of an asset name. private readonly IPrivateMethod GetKeyLocale; @@ -46,10 +53,10 @@ namespace StardewModdingAPI.Framework private readonly ContextHash AssetsBeingLoaded = new ContextHash(); /// A lookup of the content managers which loaded each asset. - private readonly IDictionary> AssetLoaders = new Dictionary>(); + private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>(); - /// An object locked to prevent concurrent changes to the underlying assets. - private readonly object Lock = new object(); + /// A lock used to prevents concurrent changes to the cache while data is being read. + private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); /********* @@ -77,30 +84,15 @@ namespace StardewModdingAPI.Framework /// The current culture for which to localise content. /// The current language code for which to localise content. /// Encapsulates monitoring and logging. - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) + /// Simplifies access to private code. + public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { - // validate - if (monitor == null) - throw new ArgumentNullException(nameof(monitor)); - - // initialise - var reflection = new Reflector(); - this.Monitor = monitor; - - // get underlying fields for interception - this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); + // init + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); - // get asset key normalisation logic - if (Constants.TargetPlatform == Platform.Windows) - { - IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); - this.NormaliseAssetNameForPlatform = path => method.Invoke(path); - } - else - this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic - // get asset data this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); @@ -108,34 +100,26 @@ namespace StardewModdingAPI.Framework /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. + [Pure] public string NormalisePathSeparators(string path) { - string[] parts = path.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - string normalised = string.Join(SContentManager.PreferredPathSeparator, parts); - if (path.StartsWith(SContentManager.PreferredPathSeparator)) - normalised = SContentManager.PreferredPathSeparator + normalised; // keep root slash - return normalised; + return this.Cache.NormalisePathSeparators(path); } /// Normalise an asset name so it's consistent with the underlying cache. /// The asset key. + [Pure] public string NormaliseAssetName(string assetName) { - assetName = this.NormalisePathSeparators(assetName); - if (assetName.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)) - return assetName.Substring(0, assetName.Length - 4); - return this.NormaliseAssetNameForPlatform(assetName); + return this.Cache.NormaliseKey(assetName); } /// Get whether the content manager has already loaded and cached the given asset. /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) { - lock (this.Lock) - { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); - } + assetName = this.Cache.NormaliseKey(assetName); + return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName)); } /// Load an asset that has been processed by the content pipeline. @@ -152,10 +136,9 @@ namespace StardewModdingAPI.Framework /// The content manager instance for which to load the asset. public T LoadFor(string assetName, ContentManager instance) { - lock (this.Lock) + assetName = this.NormaliseAssetName(assetName); + return this.WithWriteLock(() => { - assetName = this.NormaliseAssetName(assetName); - // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) { @@ -186,7 +169,7 @@ namespace StardewModdingAPI.Framework this.Cache[assetName] = data; this.TrackAssetLoader(assetName, instance); return data; - } + }); } /// Inject an asset into the cache. @@ -195,12 +178,12 @@ namespace StardewModdingAPI.Framework /// The asset value. public void Inject(string assetName, T value) { - lock (this.Lock) + this.WithWriteLock(() => { assetName = this.NormaliseAssetName(assetName); this.Cache[assetName] = value; this.TrackAssetLoader(assetName, this); - } + }); } /// Get the current content locale. @@ -212,19 +195,11 @@ namespace StardewModdingAPI.Framework /// Get the cached asset keys. public IEnumerable GetAssetKeys() { - lock (this.Lock) - { - IEnumerable GetAllAssetKeys() - { - foreach (string cacheKey in this.Cache.Keys) - { - this.ParseCacheKey(cacheKey, out string assetKey, out string _); - yield return assetKey; - } - } - - return GetAllAssetKeys().Distinct(); - } + return this.WithReadLock(() => + this.Cache.Keys + .Select(this.GetAssetName) + .Distinct() + ); } /// Purge assets from the cache that match one of the interceptors. @@ -239,11 +214,12 @@ namespace StardewModdingAPI.Framework // get CanEdit/Load methods MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + if (canEdit == null || canLoad == null) + throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen // invalidate matching keys return this.InvalidateCache((assetName, assetType) => { - // get asset metadata IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); // check loaders @@ -263,48 +239,44 @@ namespace StardewModdingAPI.Framework /// Returns whether any cache entries were invalidated. public bool InvalidateCache(Func predicate, bool dispose = false) { - lock (this.Lock) + return this.WithWriteLock(() => { - // find matching asset keys - HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string cacheKey in this.Cache.Keys) + // invalidate matching keys + HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => { - this.ParseCacheKey(cacheKey, out string assetKey, out _); - Type type = this.Cache[cacheKey].GetType(); - if (predicate(assetKey, type)) + this.ParseCacheKey(key, out string assetName, out _); + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) { - purgeAssetKeys.Add(assetKey); - purgeCacheKeys.Add(cacheKey); + removeAssetNames.Add(assetName); + removeKeys.Add(key); + return true; } - } + return false; + }); - // purge assets - foreach (string key in purgeCacheKeys) - { - if (dispose && this.Cache[key] is IDisposable disposable) - disposable.Dispose(); - this.Cache.Remove(key); - this.AssetLoaders.Remove(key); - } + // update reference tracking + foreach (string key in removeKeys) + this.ContentManagersByAssetKey.Remove(key); // reload core game assets int reloaded = 0; - foreach (string key in purgeAssetKeys) + foreach (string key in removeAssetNames) { if (this.CoreAssets.ReloadForKey(this, key)) reloaded++; } // report result - if (purgeCacheKeys.Any()) + if (removeKeys.Any()) { - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + this.Monitor.Log($"Invalidated {removeAssetNames.Count} asset names: {string.Join(", ", removeKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); return true; } this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); return false; - } + }); } /// Dispose assets for the given content manager shim. @@ -313,15 +285,26 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - foreach (var entry in this.AssetLoaders) - entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.AssetLoaders[key].Any(), dispose: true); + this.WithWriteLock(() => + { + foreach (var entry in this.ContentManagersByAssetKey) + entry.Value.Remove(shim); + this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey[key].Any(), dispose: true); + }); } /********* ** Private methods *********/ + /// Dispose held resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); + base.Dispose(disposing); + } + /// Get whether an asset has already been loaded. /// The normalised asset name. private bool IsNormalisedKeyLoaded(string normalisedAssetName) @@ -335,8 +318,8 @@ namespace StardewModdingAPI.Framework /// The content manager that loaded the asset. private void TrackAssetLoader(string key, ContentManager manager) { - if (!this.AssetLoaders.TryGetValue(key, out HashSet hash)) - hash = this.AssetLoaders[key] = new HashSet(); + if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) + hash = this.ContentManagersByAssetKey[key] = new HashSet(); hash.Add(manager); } @@ -367,11 +350,19 @@ namespace StardewModdingAPI.Framework return map; } + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + /// Parse a cache key into its component parts. /// The input cache key. - /// The original asset key. + /// The original asset name. /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) { // handle localised key if (!string.IsNullOrWhiteSpace(cacheKey)) @@ -382,7 +373,7 @@ namespace StardewModdingAPI.Framework string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); if (this.KeyLocales.ContainsKey(suffix)) { - assetKey = cacheKey.Substring(0, lastSepIndex); + assetName = cacheKey.Substring(0, lastSepIndex); localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); return; } @@ -390,7 +381,7 @@ namespace StardewModdingAPI.Framework } // handle simple key - assetKey = cacheKey; + assetName = cacheKey; localeCode = null; } @@ -519,12 +510,51 @@ namespace StardewModdingAPI.Framework } } - /// Dispose held resources. - /// Whether the content manager is disposing (rather than finalising). - protected override void Dispose(bool disposing) + /// Acquire a read lock which prevents concurrent writes to the cache while it's open. + /// The action's return value. + /// The action to perform. + private T WithReadLock(Func action) { - this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - base.Dispose(disposing); + try + { + this.Lock.EnterReadLock(); + return action(); + } + finally + { + this.Lock.ExitReadLock(); + } + } + + /// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open. + /// The action to perform. + private void WithWriteLock(Action action) + { + try + { + this.Lock.EnterWriteLock(); + action(); + } + finally + { + this.Lock.ExitWriteLock(); + } + } + + /// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open. + /// The action's return value. + /// The action to perform. + private T WithWriteLock(Func action) + { + try + { + this.Lock.EnterReadLock(); + return action(); + } + finally + { + this.Lock.ExitReadLock(); + } } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index ca19d726..c62c1393 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -179,7 +179,7 @@ namespace StardewModdingAPI.Framework // override content manager this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); - this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6f7c2b3f..605292b2 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -89,6 +89,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 68e33c653ad780f75df6642b78feca015e8dbbb2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Oct 2017 19:27:00 -0400 Subject: suppress BeforeSave, AfterSave, and AfterDayStarted events during new-game intro (#374) --- src/SMAPI/Framework/SGame.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c62c1393..c886a4b7 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -240,6 +240,9 @@ namespace StardewModdingAPI.Framework return; } + /********* + ** Save events + suppress events during save + *********/ // While the game is writing to the save file in the background, mods can unexpectedly // fail since they don't have exclusive access to resources (e.g. collection changed // during enumeration errors). To avoid problems, events are not invoked while a save @@ -248,7 +251,7 @@ namespace StardewModdingAPI.Framework if (Context.IsSaving) { // raise before-save - if (!this.IsBetweenSaveEvents) + if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); -- cgit From 749ebb912bc59fceda2f14d7e330cd9edeff19ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Oct 2017 19:54:38 -0400 Subject: 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(-) (limited to 'src') diff --git a/src/SMAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt index eb27ac52..a03ad6a4 100644 --- a/src/SMAPI.Installer/readme.txt +++ b/src/SMAPI.Installer/readme.txt @@ -1,14 +1,14 @@ - ___ ___ ___ ___ - / /\ /__/\ / /\ / /\ ___ - / /:/_ | |::\ / /::\ / /::\ / /\ - / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ - / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ /__/::\ - /__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ \__\/\:\__ - \ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \ \:\/\ - \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ - \__\/ /:/ \ \:\ \ \:\ \ \:\ /__/:/ - /__/:/ \ \:\ \ \:\ \ \:\ \__\/ - \__\/ \__\/ \__\/ \__\/ + ___ ___ ___ ___ ___ + / /\ /__/\ / /\ / /\ / /\ + / /:/_ | |::\ / /::\ / /::\ / /:/ + / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ + / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ / /::\ ___ +/__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ /__/:/\:\ /\ +\ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \__\/ \:\/:/ + \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ + \__\/ /:/ \ \:\ \ \:\ \ \:\ / /:/ + /__/:/ \ \:\ \ \:\ \ \:\ /__/:/ + \__\/ \__\/ \__\/ \__\/ \__\/ SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. -- cgit From ded647aad41d8e3591a21bdd6aa6503273312a27 Mon Sep 17 00:00:00 2001 From: Entoarox Date: Fri, 13 Oct 2017 18:19:04 +0200 Subject: PrivateProperty.cs ~ Use delegates for performance --- src/SMAPI/Framework/Reflection/PrivateProperty.cs | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 08204b7e..8a75d925 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.Reflection /// The display name shown in error messages. private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; + private readonly Func GetterDelegate; + private readonly Action SetterDelegate; + /********* ** Accessors @@ -39,20 +42,17 @@ namespace StardewModdingAPI.Framework.Reflection /// The is null for a non-static field, or not null for a static field. public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { - // validate - if (parentType == null) - throw new ArgumentNullException(nameof(parentType)); - if (property == null) - throw new ArgumentNullException(nameof(property)); if (isStatic && obj != null) throw new ArgumentException("A static property cannot have an object instance."); if (!isStatic && obj == null) throw new ArgumentException("A non-static property must have an object instance."); - // save - this.ParentType = parentType; + this.ParentType = parentType ?? throw new ArgumentNullException(nameof(parentType)); this.Parent = obj; - this.PropertyInfo = property; + this.PropertyInfo = property ?? throw new ArgumentNullException(nameof(property)); + + this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), this.PropertyInfo.GetMethod); + this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), this.PropertyInfo.SetMethod); } /// Get the property value. @@ -60,7 +60,9 @@ namespace StardewModdingAPI.Framework.Reflection { try { - return (TValue)this.PropertyInfo.GetValue(this.Parent); + return this.GetterDelegate(this.Parent); + // Old version: Commented out in case of issues with new version + //return (TValue)this.PropertyInfo.GetValue(this.Parent); } catch (InvalidCastException) { @@ -78,7 +80,9 @@ namespace StardewModdingAPI.Framework.Reflection { try { - this.PropertyInfo.SetValue(this.Parent, value); + this.SetterDelegate(this.Parent, value); + // Old version: Commented out in case of issues with new version + //this.PropertyInfo.SetValue(this.Parent, value); } catch (InvalidCastException) { -- cgit From 191d65f8d9e90cc3a9788afcae852f8879962428 Mon Sep 17 00:00:00 2001 From: Entoarox Date: Fri, 13 Oct 2017 19:00:55 +0200 Subject: Fix: Instance type is required --- src/SMAPI/Framework/Reflection/PrivateProperty.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 8a75d925..718594ee 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -51,8 +51,10 @@ namespace StardewModdingAPI.Framework.Reflection this.Parent = obj; this.PropertyInfo = property ?? throw new ArgumentNullException(nameof(property)); - this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), this.PropertyInfo.GetMethod); - this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), this.PropertyInfo.SetMethod); + Type[] types = new Type[] { this.PropertyInfo.DeclaringType, typeof(TValue)}; + + this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func<,>).MakeGenericType(types), this.PropertyInfo.GetMethod); + this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action<,>).MakeGenericType(types), this.PropertyInfo.SetMethod); } /// Get the property value. -- cgit From 7e02310a8ea9c24607a88718ee10ac5f85836fdb Mon Sep 17 00:00:00 2001 From: Entoarox Date: Mon, 23 Oct 2017 18:15:18 +0200 Subject: 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(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 718594ee..0fa10601 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Framework.Reflection /// The display name shown in error messages. private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; - private readonly Func GetterDelegate; - private readonly Action SetterDelegate; + private readonly Func GetterDelegate; + private readonly Action SetterDelegate; /********* @@ -53,8 +53,8 @@ namespace StardewModdingAPI.Framework.Reflection Type[] types = new Type[] { this.PropertyInfo.DeclaringType, typeof(TValue)}; - this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func<,>).MakeGenericType(types), this.PropertyInfo.GetMethod); - this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action<,>).MakeGenericType(types), this.PropertyInfo.SetMethod); + this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); + this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); } /// Get the property value. @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - return this.GetterDelegate(this.Parent); + return this.GetterDelegate(); // Old version: Commented out in case of issues with new version //return (TValue)this.PropertyInfo.GetValue(this.Parent); } @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - this.SetterDelegate(this.Parent, value); + this.SetterDelegate(value); // Old version: Commented out in case of issues with new version //this.PropertyInfo.SetValue(this.Parent, value); } -- cgit From f6a86e584976c87f1f678a226f8eafe6a8b9860c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Oct 2017 20:28:18 -0400 Subject: minor cleanup --- src/SMAPI/Framework/Reflection/PrivateProperty.cs | 30 +++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 0fa10601..be346d71 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; namespace StardewModdingAPI.Framework.Reflection @@ -10,16 +10,13 @@ namespace StardewModdingAPI.Framework.Reflection /********* ** Properties *********/ - /// The type that has the field. - private readonly Type ParentType; - - /// The object that has the instance field (if applicable). - private readonly object Parent; - /// The display name shown in error messages. - private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; + private readonly string DisplayName; + /// The underlying property getter. private readonly Func GetterDelegate; + + /// The underlying property setter. private readonly Action SetterDelegate; @@ -42,16 +39,21 @@ namespace StardewModdingAPI.Framework.Reflection /// The is null for a non-static field, or not null for a static field. public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { + // validate input + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (property == null) + throw new ArgumentNullException(nameof(property)); + + // validate static if (isStatic && obj != null) throw new ArgumentException("A static property cannot have an object instance."); if (!isStatic && obj == null) throw new ArgumentException("A non-static property must have an object instance."); - this.ParentType = parentType ?? throw new ArgumentNullException(nameof(parentType)); - this.Parent = obj; - this.PropertyInfo = property ?? throw new ArgumentNullException(nameof(property)); - Type[] types = new Type[] { this.PropertyInfo.DeclaringType, typeof(TValue)}; + this.DisplayName = $"{parentType.FullName}::{property.Name}"; + this.PropertyInfo = property; this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); @@ -63,8 +65,6 @@ namespace StardewModdingAPI.Framework.Reflection try { return this.GetterDelegate(); - // Old version: Commented out in case of issues with new version - //return (TValue)this.PropertyInfo.GetValue(this.Parent); } catch (InvalidCastException) { @@ -83,8 +83,6 @@ namespace StardewModdingAPI.Framework.Reflection try { this.SetterDelegate(value); - // Old version: Commented out in case of issues with new version - //this.PropertyInfo.SetValue(this.Parent, value); } catch (InvalidCastException) { -- cgit From a1eeece49b937c942e2cc002bd1863295d943fde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Oct 2017 17:14:58 -0400 Subject: centralise most content-loading logic to fix map tilesheet edge case (#373) --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 189 ++---------- src/SMAPI/Framework/SContentManager.cs | 382 +++++++++++++++++++----- src/SMAPI/IContentHelper.cs | 3 +- 3 files changed, 338 insertions(+), 236 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4f5bd2f0..2dd8a2e3 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; @@ -74,12 +72,12 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; - this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath); this.Monitor = monitor; } /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. /// Where to search for a matching content asset. /// The is empty or contains invalid characters. @@ -88,9 +86,9 @@ namespace StardewModdingAPI.Framework.ModHelpers { SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); - this.AssertValidAssetKeyFormat(key); try { + this.ContentManager.AssertValidAssetKeyFormat(key); switch (source) { case ContentSource.GameContent: @@ -103,60 +101,32 @@ namespace StardewModdingAPI.Framework.ModHelpers throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path - string assetPath = this.GetModAssetPath(key, file.FullName); + string assetName = this.GetModAssetPath(key, file.FullName); // try cache - if (this.ContentManager.IsLoaded(assetPath)) - return this.ContentManager.Load(assetPath); + if (this.ContentManager.IsLoaded(assetName)) + return this.ContentManager.Load(assetName); - // load content - switch (file.Extension.ToLower()) + // fix map tilesheets + if (file.Extension.ToLower() == ".tbin") { - // XNB file - case ".xnb": - { - T asset = this.ContentManager.Load(assetPath); - if (asset is Map) - this.FixLocalMapTilesheets(asset as Map, key); - return asset; - } - - // 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 @@ namespace StardewModdingAPI.Framework.ModHelpers try { string key = - this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource) - ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource); + this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource) + ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource); if (key != null) { tilesheet.ImageSource = key; @@ -282,33 +252,22 @@ namespace StardewModdingAPI.Framework.ModHelpers } } - /// Load a tilesheet image source if the file exists. - /// The folder path containing the map, relative to the mod folder. + /// Get the actual asset name for a tilesheet. + /// The folder path containing the map, relative to the mod folder. /// The tilesheet image source to load. - /// Returns the loaded asset key (if it was loaded successfully). + /// Returns the asset name. /// See remarks on . - private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource) + private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource) { if (imageSource == null) return null; // check relative to map file { - string localKey = Path.Combine(relativeMapFolder, imageSource); + string localKey = Path.Combine(modRelativeMapFolder, imageSource); FileInfo localFile = this.GetModFile(localKey); if (localFile.Exists) - { - try - { - this.Load(localKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex); - } - return this.GetActualAssetKey(localKey); - } } // check relative to content folder @@ -343,18 +302,6 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertValidAssetKeyFormat(string key) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new ArgumentException("The asset key or local path contains invalid characters."); - } - /// Get a file from the mod folder. /// The asset path relative to the mod folder. private FileInfo GetModFile(string path) @@ -400,81 +347,5 @@ namespace StardewModdingAPI.Framework.ModHelpers return absolutePath; #endif } - - /// Get a directory path relative to a given root. - /// The root path from which the path should be relative. - /// The target file path. - private string GetRelativePath(string rootPath, string targetPath) - { - // convert to URIs - Uri from = new Uri(rootPath + "/"); - Uri to = new Uri(targetPath + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); - - // get relative path - return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) - .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } } } diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 0b6daaa6..10d854d9 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; @@ -55,6 +59,9 @@ namespace StardewModdingAPI.Framework /// A lookup of the content managers which loaded each asset. private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>(); + /// The path prefix for assets in mod folders. + private readonly string ModContentPrefix; + /// A lock used to prevents concurrent changes to the cache while data is being read. private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); @@ -78,6 +85,9 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ + /**** + ** Constructor + ****/ /// Construct an instance. /// The service provider to use to locate services. /// The root directory to search for content. @@ -92,12 +102,16 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); + this.ModContentPrefix = this.GetRelativePath(Constants.ModPath); // get asset data this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); } + /**** + ** Asset key/name handling + ****/ /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] @@ -114,6 +128,42 @@ namespace StardewModdingAPI.Framework return this.Cache.NormaliseKey(assetName); } + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public void AssertValidAssetKeyFormat(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + } + + /// Get a directory path relative to the content root. + /// The target file path. + public string GetRelativePath(string targetPath) + { + // convert to URIs + Uri from = new Uri(this.FullRootDirectory + "/"); + Uri to = new Uri(targetPath + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'."); + + // get relative path + return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) + .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetKeyLocale.Invoke(); + } + /// Get whether the content manager has already loaded and cached the given asset. /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) @@ -122,86 +172,105 @@ namespace StardewModdingAPI.Framework return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName)); } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. + /// 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 @@ namespace StardewModdingAPI.Framework }); } + /**** + ** Disposal + ****/ /// Dispose assets for the given content manager shim. /// The content manager whose assets to dispose. internal void DisposeFor(ContentManagerShim shim) @@ -297,6 +369,9 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /**** + ** Disposal + ****/ /// Dispose held resources. /// Whether the content manager is disposing (rather than finalising). protected override void Dispose(bool disposing) @@ -305,24 +380,9 @@ namespace StardewModdingAPI.Framework base.Dispose(disposing); } - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - return this.Cache.ContainsKey(normalisedAssetName) - || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset - } - - /// Track that a content manager loaded an asset. - /// The asset key that was loaded. - /// The content manager that loaded the asset. - private void TrackAssetLoader(string key, ContentManager manager) - { - if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) - hash = this.ContentManagersByAssetKey[key] = new HashSet(); - hash.Add(manager); - } - + /**** + ** Asset name/key handling + ****/ /// Get the locale codes (like ja-JP) used in asset keys. /// Simplifies access to private game code. private IDictionary GetKeyLocales(Reflector reflection) @@ -385,6 +445,113 @@ namespace StardewModdingAPI.Framework localeCode = null; } + /**** + ** Cache handling + ****/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + private bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName) + || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset + } + + /// Track that a content manager loaded an asset. + /// The asset key that was loaded. + /// The content manager that loaded the asset. + private void TrackAssetLoader(string key, ContentManager manager) + { + if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) + hash = this.ContentManagersByAssetKey[key] = new HashSet(); + hash.Add(manager); + } + + /**** + ** Content loading + ****/ + /// Load an asset name without heuristics to support mod content. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The content manager instance for which to load the asset. + private T LoadImpl(string assetName, ContentManager instance) + { + return this.WithWriteLock(() => + { + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) + { + this.TrackAssetLoader(assetName, instance); + return base.Load(assetName); + } + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.InjectWithoutLock(assetName, data, instance); + return data; + }); + } + + /// Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + /// The content manager instance for which to load the asset. + private void InjectWithoutLock(string assetName, T value, ContentManager instance) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + this.TrackAssetLoader(assetName, instance); + } + + /// Get a file from the mod folder. + /// The asset path relative to the content folder. + private FileInfo GetModFile(string path) + { + // try exact match + FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(path + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Get a file from the game's content folder. + /// The asset key. + private FileInfo GetContentFolderFile(string key) + { + // get file path + string path = Path.Combine(this.FullRootDirectory, key); + if (!path.EndsWith(".xnb")) + path += ".xnb"; + + // get file + return new FileInfo(path); + } + /// Load the initial asset from the registered . /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. @@ -510,6 +677,69 @@ namespace StardewModdingAPI.Framework } } + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + + /**** + ** Concurrency logic + ****/ /// Acquire a read lock which prevents concurrent writes to the cache while it's open. /// The action's return value. /// The action to perform. diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index b78b165b..7900809f 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewValley; +using xTile; namespace StardewModdingAPI { @@ -29,7 +30,7 @@ namespace StardewModdingAPI ** Public methods *********/ /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. /// Where to search for a matching content asset. /// The is empty or contains invalid characters. -- cgit From a7fcfd642466b22abdc32a1f71a93e77fb8e569b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:00:03 -0400 Subject: fix incorrect search path when loading a mod file (#373) --- src/SMAPI/Framework/SContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 10d854d9..54ebba83 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -531,7 +531,7 @@ namespace StardewModdingAPI.Framework // try with default extension if (!file.Exists && file.Extension.ToLower() != ".xnb") { - FileInfo result = new FileInfo(path + ".xnb"); + FileInfo result = new FileInfo(file.FullName + ".xnb"); if (result.Exists) file = result; } -- cgit From f63484e5e76306a08e2f2f2c2f1224cc6b0af1ba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:17:25 -0400 Subject: minor cleanup (#373) --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 26 +++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 2dd8a2e3..ae812e71 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -117,7 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - this.FixLocalMapTilesheets(map, key); + this.FixCustomTilesheetPaths(map, key); // inject map this.ContentManager.Inject(assetName, map, this.ContentManager); @@ -180,25 +180,27 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Private methods *********/ - /// Fix the tilesheets for a map loaded from the mod folder. + /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. /// The map asset key within the mod folder. - /// The map tilesheets could not be loaded. + /// A map tilesheet couldn't be resolved. /// - /// The game's logic for tilesheets in is a bit specialised. It boils down to this: - /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the Content folder. + /// The game's logic for tilesheets in is a bit specialised. It boils + /// down to this: + /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded + /// as-is relative to the Content folder. /// * Else it's loaded from Content\Maps with a seasonal prefix. /// /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. /// Instead we use a more heuristic approach: check relative to the map file first, then relative to - /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, we try - /// for a seasonal variation and then an exact match. + /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, try for a + /// seasonal variation and then an exact match. /// /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. /// - private void FixLocalMapTilesheets(Map map, string mapKey) + private void FixCustomTilesheetPaths(Map map, string mapKey) { - // check map info + // get map info if (!map.TileSheets.Any()) return; mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators @@ -209,7 +211,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string imageSource = tilesheet.ImageSource; - // validate + // validate tilesheet path if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains("..")) throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); @@ -256,7 +258,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The folder path containing the map, relative to the mod folder. /// The tilesheet image source to load. /// Returns the asset name. - /// See remarks on . + /// See remarks on . private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource) { if (imageSource == null) @@ -286,7 +288,7 @@ namespace StardewModdingAPI.Framework.ModHelpers catch { // ignore file-not-found errors - // TODO: while it's useful to suppress a asset-not-found error here to avoid + // TODO: while it's useful to suppress an asset-not-found error here to avoid // confusion, this is a pretty naive approach. Even if the file doesn't exist, // the file may have been loaded through an IAssetLoader which failed. So even // if the content file doesn't exist, that doesn't mean the error here is a -- cgit From 7f16ebdb19982c182b60312883452c44fdd08fda Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 01:42:54 -0400 Subject: 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(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index a06fc0c4..ba0815b3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.1 (upcoming) * For players: * Fixed compatibility check crashing for players with Stardew Valley 1.08. + * Fixed the game's test messages being shown in the console and log. * For modders: * Added support for public code in reflection API, to simplify mod integrations. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index fe306e24..ce547d9b 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS using System.Management; @@ -77,6 +78,13 @@ namespace StardewModdingAPI /// Whether the program has been disposed. private bool IsDisposed; + /// Regex patterns which match console messages to suppress from the console and log. + private readonly Regex[] SuppressConsolePatterns = + { + new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant) + }; + /********* ** Public methods @@ -910,7 +918,14 @@ namespace StardewModdingAPI /// The message to log. private void HandleConsoleMessage(IMonitor monitor, string message) { - LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; // intercept potential exceptions + // detect exception + LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; + + // ignore suppressed message + if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) + return; + + // forward to monitor monitor.Log(message, level); } -- cgit From b945fcf5553f2df63db1fad8a73c65cd7fa7daa3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 02:44:53 -0400 Subject: fix player_setlevel command not also changing XP (#359) --- docs/release-notes.md | 1 + .../Framework/Commands/Player/SetLevelCommand.cs | 31 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ba0815b3..9366e1fc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * For players: * Fixed compatibility check crashing for players with Stardew Valley 1.08. * Fixed the game's test messages being shown in the console and log. + * Fixed TrainerMod's `player_setlevel` command not also setting XP. * For modders: * Added support for public code in reflection API, to simplify mod integrations. diff --git a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs index b223aa9f..54d5e47b 100644 --- a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs +++ b/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs @@ -1,11 +1,34 @@ -using StardewModdingAPI; +using System.Collections.Generic; +using StardewModdingAPI; using StardewValley; +using SFarmer = StardewValley.Farmer; namespace TrainerMod.Framework.Commands.Player { /// A command which edits the player's current level for a skill. internal class SetLevelCommand : TrainerCommand { + /********* + ** Properties + *********/ + /// The experience points needed to reach each level. + /// Derived from . + private readonly IDictionary LevelExp = new Dictionary + { + [0] = 0, + [1] = 100, + [2] = 380, + [3] = 770, + [4] = 1300, + [5] = 2150, + [6] = 3300, + [7] = 4800, + [8] = 6900, + [9] = 10000, + [10] = 15000 + }; + + /********* ** Public methods *********/ @@ -30,31 +53,37 @@ namespace TrainerMod.Framework.Commands.Player { case "luck": Game1.player.LuckLevel = level; + Game1.player.experiencePoints[SFarmer.luckSkill] = this.LevelExp[level]; monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info); break; case "mining": Game1.player.MiningLevel = level; + Game1.player.experiencePoints[SFarmer.miningSkill] = this.LevelExp[level]; monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info); break; case "combat": Game1.player.CombatLevel = level; + Game1.player.experiencePoints[SFarmer.combatSkill] = this.LevelExp[level]; monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info); break; case "farming": Game1.player.FarmingLevel = level; + Game1.player.experiencePoints[SFarmer.farmingSkill] = this.LevelExp[level]; monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info); break; case "fishing": Game1.player.FishingLevel = level; + Game1.player.experiencePoints[SFarmer.fishingSkill] = this.LevelExp[level]; monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info); break; case "foraging": Game1.player.ForagingLevel = level; + Game1.player.experiencePoints[SFarmer.foragingSkill] = this.LevelExp[level]; monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info); break; } -- cgit From 59dd604cf2905adf5fce7e9bb7b97886891aae81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 03:18:48 -0400 Subject: 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 | 74 +++++++++ .../Framework/Commands/ArgumentParser.cs | 158 ++++++++++++++++++ .../Framework/Commands/ITrainerCommand.cs | 32 ++++ .../Framework/Commands/Other/DebugCommand.cs | 32 ++++ .../Commands/Other/ShowDataFilesCommand.cs | 25 +++ .../Commands/Other/ShowGameFilesCommand.cs | 25 +++ .../Framework/Commands/Player/AddCommand.cs | 80 +++++++++ .../Commands/Player/ListItemTypesCommand.cs | 52 ++++++ .../Framework/Commands/Player/ListItemsCommand.cs | 75 +++++++++ .../Framework/Commands/Player/SetColorCommand.cs | 75 +++++++++ .../Framework/Commands/Player/SetHealthCommand.cs | 71 ++++++++ .../Commands/Player/SetImmunityCommand.cs | 37 +++++ .../Framework/Commands/Player/SetLevelCommand.cs | 91 +++++++++++ .../Commands/Player/SetMaxHealthCommand.cs | 37 +++++ .../Commands/Player/SetMaxStaminaCommand.cs | 37 +++++ .../Framework/Commands/Player/SetMoneyCommand.cs | 71 ++++++++ .../Framework/Commands/Player/SetNameCommand.cs | 51 ++++++ .../Framework/Commands/Player/SetSpeedCommand.cs | 30 ++++ .../Framework/Commands/Player/SetStaminaCommand.cs | 71 ++++++++ .../Framework/Commands/Player/SetStyleCommand.cs | 91 +++++++++++ .../Framework/Commands/TrainerCommand.cs | 102 ++++++++++++ .../Commands/World/DownMineLevelCommand.cs | 27 ++++ .../Framework/Commands/World/FreezeTimeCommand.cs | 66 ++++++++ .../Framework/Commands/World/SetDayCommand.cs | 38 +++++ .../Commands/World/SetMineLevelCommand.cs | 32 ++++ .../Framework/Commands/World/SetSeasonCommand.cs | 45 ++++++ .../Framework/Commands/World/SetTimeCommand.cs | 39 +++++ .../Framework/Commands/World/SetYearCommand.cs | 38 +++++ .../Framework/ItemData/ItemType.cs | 39 +++++ .../Framework/ItemData/SearchableItem.cs | 41 +++++ .../Framework/ItemRepository.cs | 179 +++++++++++++++++++++ .../Properties/AssemblyInfo.cs | 6 + .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 101 ++++++++++++ src/SMAPI.Mods.ConsoleCommands/manifest.json | 13 ++ src/SMAPI.Mods.ConsoleCommands/packages.config | 4 + src/SMAPI.sln | 4 +- .../Framework/Commands/ArgumentParser.cs | 159 ------------------ .../Framework/Commands/ITrainerCommand.cs | 34 ---- .../Framework/Commands/Other/DebugCommand.cs | 33 ---- .../Commands/Other/ShowDataFilesCommand.cs | 26 --- .../Commands/Other/ShowGameFilesCommand.cs | 26 --- .../Framework/Commands/Player/AddCommand.cs | 81 ---------- .../Commands/Player/ListItemTypesCommand.cs | 53 ------ .../Framework/Commands/Player/ListItemsCommand.cs | 76 --------- .../Framework/Commands/Player/SetColorCommand.cs | 76 --------- .../Framework/Commands/Player/SetHealthCommand.cs | 72 --------- .../Commands/Player/SetImmunityCommand.cs | 38 ----- .../Framework/Commands/Player/SetLevelCommand.cs | 92 ----------- .../Commands/Player/SetMaxHealthCommand.cs | 38 ----- .../Commands/Player/SetMaxStaminaCommand.cs | 38 ----- .../Framework/Commands/Player/SetMoneyCommand.cs | 72 --------- .../Framework/Commands/Player/SetNameCommand.cs | 52 ------ .../Framework/Commands/Player/SetSpeedCommand.cs | 31 ---- .../Framework/Commands/Player/SetStaminaCommand.cs | 72 --------- .../Framework/Commands/Player/SetStyleCommand.cs | 92 ----------- .../Framework/Commands/TrainerCommand.cs | 103 ------------ .../Commands/World/DownMineLevelCommand.cs | 28 ---- .../Framework/Commands/World/FreezeTimeCommand.cs | 67 -------- .../Framework/Commands/World/SetDayCommand.cs | 39 ----- .../Commands/World/SetMineLevelCommand.cs | 33 ---- .../Framework/Commands/World/SetSeasonCommand.cs | 46 ------ .../Framework/Commands/World/SetTimeCommand.cs | 40 ----- .../Framework/Commands/World/SetYearCommand.cs | 39 ----- src/TrainerMod/Framework/ItemData/ItemType.cs | 39 ----- .../Framework/ItemData/SearchableItem.cs | 41 ----- src/TrainerMod/Framework/ItemRepository.cs | 179 --------------------- src/TrainerMod/Properties/AssemblyInfo.cs | 6 - src/TrainerMod/TrainerMod.cs | 75 --------- src/TrainerMod/TrainerMod.csproj | 101 ------------ src/TrainerMod/manifest.json | 13 -- src/TrainerMod/packages.config | 4 - 74 files changed, 1994 insertions(+), 2021 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj create mode 100644 src/SMAPI.Mods.ConsoleCommands/manifest.json create mode 100644 src/SMAPI.Mods.ConsoleCommands/packages.config delete mode 100644 src/TrainerMod/Framework/Commands/ArgumentParser.cs delete mode 100644 src/TrainerMod/Framework/Commands/ITrainerCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Other/DebugCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/AddCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/TrainerCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/SetDayCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/World/SetYearCommand.cs delete mode 100644 src/TrainerMod/Framework/ItemData/ItemType.cs delete mode 100644 src/TrainerMod/Framework/ItemData/SearchableItem.cs delete mode 100644 src/TrainerMod/Framework/ItemRepository.cs delete mode 100644 src/TrainerMod/Properties/AssemblyInfo.cs delete mode 100644 src/TrainerMod/TrainerMod.cs delete mode 100644 src/TrainerMod/TrainerMod.csproj delete mode 100644 src/TrainerMod/manifest.json delete mode 100644 src/TrainerMod/packages.config (limited to 'src') diff --git a/build/common.targets b/build/common.targets index ee138524..aa11344e 100644 --- a/build/common.targets +++ b/build/common.targets @@ -78,7 +78,7 @@ - + @@ -89,10 +89,10 @@ - - - - + + + + diff --git a/docs/release-notes.md b/docs/release-notes.md index 9366e1fc..1202407f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ * Fixed compatibility check crashing for players with Stardew Valley 1.08. * Fixed the game's test messages being shown in the console and log. * Fixed TrainerMod's `player_setlevel` command not also setting XP. + * Renamed the default _TrainerMod_ mod to _Console Commands_ to clarify its purpose. * For modders: * Added support for public code in reflection API, to simplify mod integrations. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 1a132e54..cbc8a401 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -97,6 +97,7 @@ namespace StardewModdingApi.Installer // obsolete yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 + yield return GetInstallPath("Mods/TrainerMod"); // *–2.0 (renamed to ConsoleCommands) yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 if (modsDir.Exists) diff --git a/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs b/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs new file mode 100644 index 00000000..96658928 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/ConsoleCommandsMod.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Events; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; + +namespace StardewModdingAPI.Mods.ConsoleCommands +{ + /// The main entry point for the mod. + public class ConsoleCommandsMod : Mod + { + /********* + ** Properties + *********/ + /// The commands to handle. + private ITrainerCommand[] Commands; + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + // register commands + this.Commands = this.ScanForCommands().ToArray(); + foreach (ITrainerCommand command in this.Commands) + helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); + + // hook events + GameEvents.UpdateTick += this.GameEvents_UpdateTick; + } + + + /********* + ** Private methods + *********/ + /// The method invoked when the game updates its state. + /// The event sender. + /// The event arguments. + private void GameEvents_UpdateTick(object sender, EventArgs e) + { + if (!Context.IsWorldReady) + return; + + foreach (ITrainerCommand command in this.Commands) + { + if (command.NeedsUpdate) + command.Update(this.Monitor); + } + } + + /// Handle a console command. + /// The command to invoke. + /// The command name specified by the user. + /// The command arguments. + private void HandleCommand(ITrainerCommand command, string commandName, string[] args) + { + ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor); + command.Handle(this.Monitor, commandName, argParser); + } + + /// Find all commands in the assembly. + private IEnumerable ScanForCommands() + { + return ( + from type in this.GetType().Assembly.GetTypes() + where !type.IsAbstract && typeof(ITrainerCommand).IsAssignableFrom(type) + select (ITrainerCommand)Activator.CreateInstance(type) + ); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs new file mode 100644 index 00000000..3ad1e168 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands +{ + /// Provides methods for parsing command-line arguments. + internal class ArgumentParser : IReadOnlyList + { + /********* + ** Properties + *********/ + /// The command name for errors. + private readonly string CommandName; + + /// The arguments to parse. + private readonly string[] Args; + + /// Writes messages to the console and log file. + private readonly IMonitor Monitor; + + + /********* + ** Accessors + *********/ + /// Get the number of arguments. + public int Count => this.Args.Length; + + /// Get the argument at the specified index in the list. + /// The zero-based index of the element to get. + public string this[int index] => this.Args[index]; + + /// A method which parses a string argument into the given value. + /// The expected argument type. + /// The argument to parse. + /// The parsed value. + /// Returns whether the argument was successfully parsed. + public delegate bool ParseDelegate(string input, out T output); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The command name for errors. + /// The arguments to parse. + /// Writes messages to the console and log file. + public ArgumentParser(string commandName, string[] args, IMonitor monitor) + { + this.CommandName = commandName; + this.Args = args; + this.Monitor = monitor; + } + + /// Try to read a string argument. + /// The argument index. + /// The argument name for error messages. + /// The parsed value. + /// Whether to show an error if the argument is missing. + /// Require that the argument match one of the given values (case-insensitive). + public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null) + { + value = null; + + // validate + if (this.Args.Length < index + 1) + { + if (required) + this.LogError($"Argument {index} ({name}) is required."); + return false; + } + if (oneOf?.Any() == true && !oneOf.Contains(this.Args[index], StringComparer.InvariantCultureIgnoreCase)) + { + this.LogError($"Argument {index} ({name}) must be one of {string.Join(", ", oneOf)}."); + return false; + } + + // get value + value = this.Args[index]; + return true; + } + + /// Try to read an integer argument. + /// The argument index. + /// The argument name for error messages. + /// The parsed value. + /// Whether to show an error if the argument is missing. + /// The minimum value allowed. + /// The maximum value allowed. + public bool TryGetInt(int index, string name, out int value, bool required = true, int? min = null, int? max = null) + { + value = 0; + + // get argument + if (!this.TryGet(index, name, out string raw, required)) + return false; + + // parse + if (!int.TryParse(raw, out value)) + { + this.LogIntFormatError(index, name, min, max); + return false; + } + + // validate + if ((min.HasValue && value < min) || (max.HasValue && value > max)) + { + this.LogIntFormatError(index, name, min, max); + return false; + } + + return true; + } + + /// Returns an enumerator that iterates through the collection. + /// An enumerator that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return ((IEnumerable)this.Args).GetEnumerator(); + } + + /// Returns an enumerator that iterates through a collection. + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + + /********* + ** Private methods + *********/ + /// Log a usage error. + /// The message describing the error. + private void LogError(string message) + { + this.Monitor.Log($"{message} Type 'help {this.CommandName}' for usage.", LogLevel.Error); + } + + /// Print an error for an invalid int argument. + /// The argument index. + /// The argument name for error messages. + /// The minimum value allowed. + /// The maximum value allowed. + private void LogIntFormatError(int index, string name, int? min, int? max) + { + if (min.HasValue && max.HasValue) + this.LogError($"Argument {index} ({name}) must be an integer between {min} and {max}."); + else if (min.HasValue) + this.LogError($"Argument {index} ({name}) must be an integer and at least {min}."); + else if (max.HasValue) + this.LogError($"Argument {index} ({name}) must be an integer and at most {max}."); + else + this.LogError($"Argument {index} ({name}) must be an integer."); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs new file mode 100644 index 00000000..a0b739f8 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs @@ -0,0 +1,32 @@ +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands +{ + /// A console command to register. + internal interface ITrainerCommand + { + /********* + ** Accessors + *********/ + /// The command name the user must type. + string Name { get; } + + /// The command description. + string Description { get; } + + /// Whether the command needs to perform logic when the game updates. + bool NeedsUpdate { get; } + + + /********* + ** Public methods + *********/ + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + void Handle(IMonitor monitor, string command, ArgumentParser args); + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + void Update(IMonitor monitor); + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs new file mode 100644 index 00000000..e4010111 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs @@ -0,0 +1,32 @@ +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// A command which sends a debug command to the game. + internal class DebugCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public DebugCommand() + : base("debug", "Run one of the game's debug commands; for example, 'debug warp FarmHouse 1 1' warps the player to the farmhouse.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // submit command + string debugCommand = string.Join(" ", args); + string oldOutput = Game1.debugOutput; + Game1.game1.parseDebugInput(debugCommand); + + // show result + monitor.Log(Game1.debugOutput != oldOutput + ? $"> {Game1.debugOutput}" + : "Sent debug command to the game, but there was no output.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs new file mode 100644 index 00000000..54d27185 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// A command which shows the data files. + internal class ShowDataFilesCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public ShowDataFilesCommand() + : base("show_data_files", "Opens the folder containing the save and log files.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + Process.Start(Constants.DataPath); + monitor.Log($"OK, opening {Constants.DataPath}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs new file mode 100644 index 00000000..0257892f --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// A command which shows the game files. + internal class ShowGameFilesCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public ShowGameFilesCommand() + : base("show_game_files", "Opens the game folder.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + Process.Start(Constants.ExecutionPath); + monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs new file mode 100644 index 00000000..81167747 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which adds an item to the player inventory. + internal class AddCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// Provides methods for searching and constructing items. + private readonly ItemRepository Items = new ItemRepository(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public AddCommand() + : base("player_add", AddCommand.GetDescription()) + { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // read arguments + if (!args.TryGet(0, "item type", out string rawType, oneOf: Enum.GetNames(typeof(ItemType)))) + return; + if (!args.TryGetInt(1, "item ID", out int id, min: 0)) + return; + if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) + count = 1; + if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) + quality = Object.lowQuality; + ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true); + + // find matching item + SearchableItem match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); + if (match == null) + { + monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); + return; + } + + // apply count & quality + match.Item.Stack = count; + if (match.Item is Object obj) + obj.quality = quality; + + // add to inventory + Game1.player.addItemByMenuIfNecessary(match.Item); + monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); + } + + /********* + ** Private methods + *********/ + private static string GetDescription() + { + string[] typeValues = Enum.GetNames(typeof(ItemType)); + return "Gives the player an item.\n" + + "\n" + + "Usage: player_add [count] [quality]\n" + + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" + + "- item: the item ID (use the 'list_items' command to see a list).\n" + + "- count (optional): how many of the item to give.\n" + + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + + "\n" + + "This example adds the galaxy sword to your inventory:\n" + + " player_add weapon 4"; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs new file mode 100644 index 00000000..34f1760c --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs @@ -0,0 +1,52 @@ +using System.Linq; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which list item types. + internal class ListItemTypesCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// Provides methods for searching and constructing items. + private readonly ItemRepository Items = new ItemRepository(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ListItemTypesCommand() + : base("list_item_types", "Lists item types you can filter in other commands.\n\nUsage: list_item_types") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + + // handle + ItemType[] matches = + ( + from item in this.Items.GetAll() + orderby item.Type.ToString() + select item.Type + ) + .Distinct() + .ToArray(); + string summary = "Searching...\n"; + if (matches.Any()) + monitor.Log(summary + this.GetTableString(matches, new[] { "type" }, val => new[] { val.ToString() }), LogLevel.Info); + else + monitor.Log(summary + "No item types found.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs new file mode 100644 index 00000000..942a50b8 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which list items available to spawn. + internal class ListItemsCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// Provides methods for searching and constructing items. + private readonly ItemRepository Items = new ItemRepository(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ListItemsCommand() + : base("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + + // handle + SearchableItem[] matches = + ( + from item in this.GetItems(args.ToArray()) + orderby item.Type.ToString(), item.Name + select item + ) + .ToArray(); + string summary = "Searching...\n"; + if (matches.Any()) + monitor.Log(summary + this.GetTableString(matches, new[] { "type", "name", "id" }, val => new[] { val.Type.ToString(), val.Name, val.ID.ToString() }), LogLevel.Info); + else + monitor.Log(summary + "No items found", LogLevel.Info); + } + + + /********* + ** Private methods + *********/ + /// Get all items which can be searched and added to the player's inventory through the console. + /// The search string to find. + private IEnumerable GetItems(string[] searchWords) + { + // normalise search term + searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); + if (searchWords?.Any() == false) + searchWords = null; + + // find matches + return ( + from item in this.Items.GetAll() + let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}" + where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1) + select item + ); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs new file mode 100644 index 00000000..5d098593 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -0,0 +1,75 @@ +using Microsoft.Xna.Framework; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the color of a player feature. + internal class SetColorCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetColorCommand() + : base("player_changecolor", "Sets the color of a player feature.\n\nUsage: player_changecolor \n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- color: a color value in RGB format, like (255,255,255).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse arguments + if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "eyes", "pants" })) + return; + if (!args.TryGet(1, "color", out string rawColor)) + return; + + // parse color + if (!this.TryParseColor(rawColor, out Color color)) + { + this.LogUsageError(monitor, "Argument 1 (color) must be an RBG value like '255,150,0'."); + return; + } + + // handle + switch (target) + { + case "hair": + Game1.player.hairstyleColor = color; + monitor.Log("OK, your hair color is updated.", LogLevel.Info); + break; + + case "eyes": + Game1.player.changeEyeColor(color); + monitor.Log("OK, your eye color is updated.", LogLevel.Info); + break; + + case "pants": + Game1.player.pantsColor = color; + monitor.Log("OK, your pants color is updated.", LogLevel.Info); + break; + } + } + + + /********* + ** Private methods + *********/ + /// Try to parse a color from a string. + /// The input string. + /// The color to set. + private bool TryParseColor(string input, out Color color) + { + string[] colorHexes = input.Split(new[] { ',' }, 3); + if (int.TryParse(colorHexes[0], out int r) && int.TryParse(colorHexes[1], out int g) && int.TryParse(colorHexes[2], out int b)) + { + color = new Color(r, g, b); + return true; + } + + color = Color.Transparent; + return false; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs new file mode 100644 index 00000000..2e8f6630 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -0,0 +1,71 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's current health. + internal class SetHealthCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// Whether to keep the player's health at its maximum. + private bool InfiniteHealth; + + + /********* + ** Accessors + *********/ + /// Whether the command needs to perform logic when the game updates. + public override bool NeedsUpdate => this.InfiniteHealth; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetHealthCommand() + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // no-argument mode + if (!args.Any()) + { + monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info); + return; + } + + // handle + string amountStr = args[0]; + if (amountStr == "inf") + { + this.InfiniteHealth = true; + monitor.Log("OK, you now have infinite health.", LogLevel.Info); + } + else + { + this.InfiniteHealth = false; + if (int.TryParse(amountStr, out int amount)) + { + Game1.player.health = amount; + monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info); + } + else + this.LogArgumentNotInt(monitor); + } + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void Update(IMonitor monitor) + { + if (this.InfiniteHealth) + Game1.player.health = Game1.player.maxHealth; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs new file mode 100644 index 00000000..9c66c4fe --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs @@ -0,0 +1,37 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's current immunity. + internal class SetImmunityCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetImmunityCommand() + : base("player_setimmunity", "Sets the player's immunity.\n\nUsage: player_setimmunity [value]\n- value: an integer amount.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!args.Any()) + { + monitor.Log($"You currently have {Game1.player.immunity} immunity. Specify a value to change it.", LogLevel.Info); + return; + } + + // handle + if (args.TryGetInt(0, "amount", out int amount, min: 0)) + { + Game1.player.immunity = amount; + monitor.Log($"OK, you now have {Game1.player.immunity} immunity.", LogLevel.Info); + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs new file mode 100644 index 00000000..68891267 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using StardewValley; +using SFarmer = StardewValley.Farmer; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's current level for a skill. + internal class SetLevelCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// The experience points needed to reach each level. + /// Derived from . + private readonly IDictionary LevelExp = new Dictionary + { + [0] = 0, + [1] = 100, + [2] = 380, + [3] = 770, + [4] = 1300, + [5] = 2150, + [6] = 3300, + [7] = 4800, + [8] = 6900, + [9] = 10000, + [10] = 15000 + }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetLevelCommand() + : base("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel \n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!args.TryGet(0, "skill", out string skill, oneOf: new[] { "luck", "mining", "combat", "farming", "fishing", "foraging" })) + return; + if (!args.TryGetInt(1, "level", out int level, min: 0, max: 10)) + return; + + // handle + switch (skill) + { + case "luck": + Game1.player.LuckLevel = level; + Game1.player.experiencePoints[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; + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs new file mode 100644 index 00000000..f4ae0694 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs @@ -0,0 +1,37 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's maximum health. + internal class SetMaxHealthCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetMaxHealthCommand() + : base("player_setmaxhealth", "Sets the player's max health.\n\nUsage: player_setmaxhealth [value]\n- value: an integer amount.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!args.Any()) + { + monitor.Log($"You currently have {Game1.player.maxHealth} max health. Specify a value to change it.", LogLevel.Info); + return; + } + + // handle + if (args.TryGetInt(0, "amount", out int amount, min: 1)) + { + Game1.player.maxHealth = amount; + monitor.Log($"OK, you now have {Game1.player.maxHealth} max health.", LogLevel.Info); + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs new file mode 100644 index 00000000..5bce5ea3 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs @@ -0,0 +1,37 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's maximum stamina. + internal class SetMaxStaminaCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetMaxStaminaCommand() + : base("player_setmaxstamina", "Sets the player's max stamina.\n\nUsage: player_setmaxstamina [value]\n- value: an integer amount.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!args.Any()) + { + monitor.Log($"You currently have {Game1.player.MaxStamina} max stamina. Specify a value to change it.", LogLevel.Info); + return; + } + + // handle + if (args.TryGetInt(0, "amount", out int amount, min: 1)) + { + Game1.player.MaxStamina = amount; + monitor.Log($"OK, you now have {Game1.player.MaxStamina} max stamina.", LogLevel.Info); + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs new file mode 100644 index 00000000..3fc504b1 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -0,0 +1,71 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's current money. + internal class SetMoneyCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// Whether to keep the player's money at a set value. + private bool InfiniteMoney; + + + /********* + ** Accessors + *********/ + /// Whether the command needs to perform logic when the game updates. + public override bool NeedsUpdate => this.InfiniteMoney; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetMoneyCommand() + : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!args.Any()) + { + monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info); + return; + } + + // handle + string amountStr = args[0]; + if (amountStr == "inf") + { + this.InfiniteMoney = true; + monitor.Log("OK, you now have infinite money.", LogLevel.Info); + } + else + { + this.InfiniteMoney = false; + if (int.TryParse(amountStr, out int amount)) + { + Game1.player.Money = amount; + monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info); + } + else + this.LogArgumentNotInt(monitor); + } + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void Update(IMonitor monitor) + { + if (this.InfiniteMoney) + Game1.player.money = 999999; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs new file mode 100644 index 00000000..5b1225e8 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -0,0 +1,51 @@ +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's name. + internal class SetNameCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetNameCommand() + : base("player_setname", "Sets the player's name.\n\nUsage: player_setname \n- target: what to rename (one of 'player' or 'farm').\n- name: the new name to set.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse arguments + if (!args.TryGet(0, "target", out string target, oneOf: new[] { "player", "farm" })) + return; + args.TryGet(1, "name", out string name, required: false); + + // handle + switch (target) + { + case "player": + if (!string.IsNullOrWhiteSpace(name)) + { + Game1.player.Name = args[1]; + monitor.Log($"OK, your name is now {Game1.player.Name}.", LogLevel.Info); + } + else + monitor.Log($"Your name is currently '{Game1.player.Name}'. Type 'help player_setname' for usage.", LogLevel.Info); + break; + + case "farm": + if (!string.IsNullOrWhiteSpace(name)) + { + Game1.player.farmName = args[1]; + monitor.Log($"OK, your farm's name is now {Game1.player.farmName}.", LogLevel.Info); + } + else + monitor.Log($"Your farm's name is currently '{Game1.player.farmName}'. Type 'help player_setname' for usage.", LogLevel.Info); + break; + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs new file mode 100644 index 00000000..e9693540 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetSpeedCommand.cs @@ -0,0 +1,30 @@ +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's current added speed. + internal class SetSpeedCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetSpeedCommand() + : base("player_setspeed", "Sets the player's added speed to the specified value.\n\nUsage: player_setspeed \n- value: an integer amount (0 is normal).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse arguments + if (!args.TryGetInt(0, "added speed", out int amount, min: 0)) + return; + + // handle + Game1.player.addedSpeed = amount; + monitor.Log($"OK, your added speed is now {Game1.player.addedSpeed}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs new file mode 100644 index 00000000..866c3d22 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -0,0 +1,71 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits the player's current stamina. + internal class SetStaminaCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// Whether to keep the player's stamina at its maximum. + private bool InfiniteStamina; + + + /********* + ** Accessors + *********/ + /// Whether the command needs to perform logic when the game updates. + public override bool NeedsUpdate => this.InfiniteStamina; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetStaminaCommand() + : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!args.Any()) + { + monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info); + return; + } + + // handle + string amountStr = args[0]; + if (amountStr == "inf") + { + this.InfiniteStamina = true; + monitor.Log("OK, you now have infinite stamina.", LogLevel.Info); + } + else + { + this.InfiniteStamina = false; + if (int.TryParse(amountStr, out int amount)) + { + Game1.player.Stamina = amount; + monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info); + } + else + this.LogArgumentNotInt(monitor); + } + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void Update(IMonitor monitor) + { + if (this.InfiniteStamina) + Game1.player.stamina = Game1.player.MaxStamina; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs new file mode 100644 index 00000000..b59be2e5 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -0,0 +1,91 @@ +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// A command which edits a player style. + internal class SetStyleCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetStyleCommand() + : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse arguments + if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" })) + return; + if (!args.TryGetInt(1, "style ID", out int styleID)) + return; + + // handle + switch (target) + { + case "hair": + Game1.player.changeHairStyle(styleID); + monitor.Log("OK, your hair style is updated.", LogLevel.Info); + break; + + case "shirt": + Game1.player.changeShirt(styleID); + monitor.Log("OK, your shirt style is updated.", LogLevel.Info); + break; + + case "acc": + Game1.player.changeAccessory(styleID); + monitor.Log("OK, your accessory style is updated.", LogLevel.Info); + break; + + case "skin": + Game1.player.changeSkinColor(styleID); + monitor.Log("OK, your skin color is updated.", LogLevel.Info); + break; + + case "shoe": + Game1.player.changeShoeColor(styleID); + monitor.Log("OK, your shoe style is updated.", LogLevel.Info); + break; + + case "swim": + switch (styleID) + { + case 0: + Game1.player.changeOutOfSwimSuit(); + monitor.Log("OK, you're no longer in your swimming suit.", LogLevel.Info); + break; + case 1: + Game1.player.changeIntoSwimsuit(); + monitor.Log("OK, you're now in your swimming suit.", LogLevel.Info); + break; + default: + this.LogUsageError(monitor, "The swim value should be 0 (no swimming suit) or 1 (swimming suit)."); + break; + } + break; + + case "gender": + switch (styleID) + { + case 0: + Game1.player.changeGender(true); + monitor.Log("OK, you're now male.", LogLevel.Info); + break; + case 1: + Game1.player.changeGender(false); + monitor.Log("OK, you're now female.", LogLevel.Info); + break; + default: + this.LogUsageError(monitor, "The gender value should be 0 (male) or 1 (female)."); + break; + } + break; + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs new file mode 100644 index 00000000..466b8f6e --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands +{ + /// The base implementation for a trainer command. + internal abstract class TrainerCommand : ITrainerCommand + { + /********* + ** Accessors + *********/ + /// The command name the user must type. + public string Name { get; } + + /// The command description. + public string Description { get; } + + /// Whether the command needs to perform logic when the game updates. + public virtual bool NeedsUpdate { get; } = false; + + + /********* + ** Public methods + *********/ + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public abstract void Handle(IMonitor monitor, string command, ArgumentParser args); + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public virtual void Update(IMonitor monitor) { } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The command name the user must type. + /// The command description. + protected TrainerCommand(string name, string description) + { + this.Name = name; + this.Description = description; + } + + /// Log an error indicating incorrect usage. + /// Writes messages to the console and log file. + /// A sentence explaining the problem. + protected void LogUsageError(IMonitor monitor, string error) + { + monitor.Log($"{error} Type 'help {this.Name}' for usage.", LogLevel.Error); + } + + /// Log an error indicating a value must be an integer. + /// Writes messages to the console and log file. + protected void LogArgumentNotInt(IMonitor monitor) + { + this.LogUsageError(monitor, "The value must be a whole number."); + } + + /// Get an ASCII table to show tabular data in the console. + /// The data type. + /// The data to display. + /// The table header. + /// Returns a set of fields for a data value. + protected string GetTableString(IEnumerable data, string[] header, Func getRow) + { + // get table data + int[] widths = header.Select(p => p.Length).ToArray(); + string[][] rows = data + .Select(item => + { + string[] fields = getRow(item); + if (fields.Length != widths.Length) + throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); + + for (int i = 0; i < fields.Length; i++) + widths[i] = Math.Max(widths[i], fields[i].Length); + + return fields; + }) + .ToArray(); + + // render fields + List lines = new List(rows.Length + 2) + { + header, + header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() + }; + lines.AddRange(rows); + + return string.Join( + Environment.NewLine, + lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) + ) + ); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs new file mode 100644 index 00000000..da117006 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs @@ -0,0 +1,27 @@ +using StardewValley; +using StardewValley.Locations; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which moves the player to the next mine level. + internal class DownMineLevelCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public DownMineLevelCommand() + : base("world_downminelevel", "Goes down one mine level.") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + int level = (Game1.currentLocation as MineShaft)?.mineLevel ?? 0; + monitor.Log($"OK, warping you to mine level {level + 1}.", LogLevel.Info); + Game1.enterMine(false, level + 1, ""); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs new file mode 100644 index 00000000..2627b714 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -0,0 +1,66 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which freezes the current time. + internal class FreezeTimeCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// The time of day at which to freeze time. + internal static int FrozenTime; + + /// Whether to freeze time. + private bool FreezeTime; + + + /********* + ** Accessors + *********/ + /// Whether the command needs to perform logic when the game updates. + public override bool NeedsUpdate => this.FreezeTime; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public FreezeTimeCommand() + : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + if (args.Any()) + { + // parse arguments + if (!args.TryGetInt(0, "value", out int value, min: 0, max: 1)) + return; + + // handle + this.FreezeTime = value == 1; + FreezeTimeCommand.FrozenTime = Game1.timeOfDay; + monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info); + } + else + { + this.FreezeTime = !this.FreezeTime; + FreezeTimeCommand.FrozenTime = Game1.timeOfDay; + monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info); + } + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void Update(IMonitor monitor) + { + if (this.FreezeTime) + Game1.timeOfDay = FreezeTimeCommand.FrozenTime; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs new file mode 100644 index 00000000..8d6bd759 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -0,0 +1,38 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which sets the current day. + internal class SetDayCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetDayCommand() + : base("world_setday", "Sets the day to the specified value.\n\nUsage: world_setday .\n- value: the target day (a number from 1 to 28).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // no-argument mode + if (!args.Any()) + { + monitor.Log($"The current date is {Game1.currentSeason} {Game1.dayOfMonth}. Specify a value to change the day.", LogLevel.Info); + return; + } + + // parse arguments + if (!args.TryGetInt(0, "day", out int day, min: 1, max: 28)) + return; + + // handle + Game1.dayOfMonth = day; + monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs new file mode 100644 index 00000000..1024b7b6 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -0,0 +1,32 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which moves the player to the given mine level. + internal class SetMineLevelCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetMineLevelCommand() + : base("world_setminelevel", "Sets the mine level?\n\nUsage: world_setminelevel \n- value: The target level (a number starting at 1).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse arguments + if (!args.TryGetInt(0, "mine level", out int level, min: 1)) + return; + + // handle + level = Math.Max(1, level); + monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info); + Game1.enterMine(true, level, ""); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs new file mode 100644 index 00000000..897d052f --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -0,0 +1,45 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which sets the current season. + internal class SetSeasonCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// The valid season names. + private readonly string[] ValidSeasons = { "winter", "spring", "summer", "fall" }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetSeasonCommand() + : base("world_setseason", "Sets the season to the specified value.\n\nUsage: world_setseason \n- season: the target season (one of 'spring', 'summer', 'fall', 'winter').") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // no-argument mode + if (!args.Any()) + { + monitor.Log($"The current season is {Game1.currentSeason}. Specify a value to change it.", LogLevel.Info); + return; + } + + // parse arguments + if (!args.TryGet(0, "season", out string season, oneOf: this.ValidSeasons)) + return; + + // handle + Game1.currentSeason = season; + monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs new file mode 100644 index 00000000..d6c71387 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -0,0 +1,39 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which sets the current time. + internal class SetTimeCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetTimeCommand() + : base("world_settime", "Sets the time to the specified value.\n\nUsage: world_settime \n- value: the target time in military time (like 0600 for 6am and 1800 for 6pm).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // no-argument mode + if (!args.Any()) + { + monitor.Log($"The current time is {Game1.timeOfDay}. Specify a value to change it.", LogLevel.Info); + return; + } + + // parse arguments + if (!args.TryGetInt(0, "time", out int time, min: 600, max: 2600)) + return; + + // handle + Game1.timeOfDay = time; + FreezeTimeCommand.FrozenTime = Game1.timeOfDay; + monitor.Log($"OK, the time is now {Game1.timeOfDay.ToString().PadLeft(4, '0')}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs new file mode 100644 index 00000000..66abd6dc --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -0,0 +1,38 @@ +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// A command which sets the current year. + internal class SetYearCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SetYearCommand() + : base("world_setyear", "Sets the year to the specified value.\n\nUsage: world_setyear \n- year: the target year (a number starting from 1).") { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // no-argument mode + if (!args.Any()) + { + monitor.Log($"The current year is {Game1.year}. Specify a value to change the year.", LogLevel.Info); + return; + } + + // parse arguments + if (!args.TryGetInt(0, "year", out int year, min: 1)) + return; + + // handle + Game1.year = year; + monitor.Log($"OK, the year is now {Game1.year}.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs new file mode 100644 index 00000000..797d4650 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs @@ -0,0 +1,39 @@ +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData +{ + /// An item type that can be searched and added to the player through the console. + internal enum ItemType + { + /// A big craftable object in + BigCraftable, + + /// A item. + Boots, + + /// A fish item. + Fish, + + /// A flooring item. + Flooring, + + /// A item. + Furniture, + + /// A item. + Hat, + + /// Any object in (except rings). + Object, + + /// A item. + Ring, + + /// A tool. + Tool, + + /// A wall item. + Wallpaper, + + /// A or item. + Weapon + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs new file mode 100644 index 00000000..3eede413 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs @@ -0,0 +1,41 @@ +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData +{ + /// A game item with metadata. + internal class SearchableItem + { + /********* + ** Accessors + *********/ + /// The item type. + public ItemType Type { get; } + + /// The item instance. + public Item Item { get; } + + /// The item's unique ID for its type. + public int ID { get; } + + /// The item's default name. + public string Name => this.Item.Name; + + /// The item's display name for the current language. + public string DisplayName => this.Item.DisplayName; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The item type. + /// The unique ID (if different from the item's parent sheet index). + /// The item instance. + public SearchableItem(ItemType type, int id, Item item) + { + this.Type = type; + this.ID = id; + this.Item = item; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs new file mode 100644 index 00000000..b5fe9f2f --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; +using StardewValley; +using StardewValley.Objects; +using StardewValley.Tools; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework +{ + /// Provides methods for searching and constructing items. + internal class ItemRepository + { + /********* + ** Properties + *********/ + /// The custom ID offset for items don't have a unique ID in the game. + private readonly int CustomIDOffset = 1000; + + + /********* + ** Public methods + *********/ + /// Get all spawnable items. + public IEnumerable GetAll() + { + // get tools + for (int quality = Tool.stone; quality <= Tool.iridium; quality++) + { + yield return new SearchableItem(ItemType.Tool, ToolFactory.axe, ToolFactory.getToolFromDescription(ToolFactory.axe, quality)); + yield return new SearchableItem(ItemType.Tool, ToolFactory.hoe, ToolFactory.getToolFromDescription(ToolFactory.hoe, quality)); + yield return new SearchableItem(ItemType.Tool, ToolFactory.pickAxe, ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality)); + yield return new SearchableItem(ItemType.Tool, ToolFactory.wateringCan, ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality)); + if (quality != Tool.iridium) + yield return new SearchableItem(ItemType.Tool, ToolFactory.fishingRod, ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality)); + } + yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset, new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones + yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 1, new Shears()); + yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 2, new Pan()); + + // wallpapers + for (int id = 0; id < 112; id++) + yield return new SearchableItem(ItemType.Wallpaper, id, new Wallpaper(id)); + + // flooring + for (int id = 0; id < 40; id++) + yield return new SearchableItem(ItemType.Flooring, id, new Wallpaper(id, isFloor: true)); + + // equipment + foreach (int id in Game1.content.Load>("Data\\Boots").Keys) + yield return new SearchableItem(ItemType.Boots, id, new Boots(id)); + foreach (int id in Game1.content.Load>("Data\\hats").Keys) + yield return new SearchableItem(ItemType.Hat, id, new Hat(id)); + foreach (int id in Game1.objectInformation.Keys) + { + if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange) + yield return new SearchableItem(ItemType.Ring, id, new Ring(id)); + } + + // weapons + foreach (int id in Game1.content.Load>("Data\\weapons").Keys) + { + Item weapon = (id >= 32 && id <= 34) + ? (Item)new Slingshot(id) + : new MeleeWeapon(id); + yield return new SearchableItem(ItemType.Weapon, id, weapon); + } + + // furniture + foreach (int id in Game1.content.Load>("Data\\Furniture").Keys) + { + if (id == 1466 || id == 1468) + yield return new SearchableItem(ItemType.Furniture, id, new TV(id, Vector2.Zero)); + else + yield return new SearchableItem(ItemType.Furniture, id, new Furniture(id, Vector2.Zero)); + } + + // fish + foreach (int id in Game1.content.Load>("Data\\Fish").Keys) + yield return new SearchableItem(ItemType.Fish, id, new SObject(id, 999)); + + // craftables + foreach (int id in Game1.bigCraftablesInformation.Keys) + yield return new SearchableItem(ItemType.BigCraftable, id, new SObject(Vector2.Zero, id)); + + // objects + foreach (int id in Game1.objectInformation.Keys) + { + if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange) + continue; // handled separated + + SObject item = new SObject(id, 1); + yield return new SearchableItem(ItemType.Object, id, item); + + // fruit products + if (item.category == SObject.FruitsCategory) + { + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, new SObject(348, 1) + { + name = $"{item.Name} Wine", + price = item.price * 3, + preserve = SObject.PreserveType.Wine, + preservedParentSheetIndex = item.parentSheetIndex + }); + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, new SObject(344, 1) + { + name = $"{item.Name} Jelly", + price = 50 + item.Price * 2, + preserve = SObject.PreserveType.Jelly, + preservedParentSheetIndex = item.parentSheetIndex + }); + } + + // vegetable products + else if (item.category == SObject.VegetableCategory) + { + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, new SObject(350, 1) + { + name = $"{item.Name} Juice", + price = (int)(item.price * 2.25d), + preserve = SObject.PreserveType.Juice, + preservedParentSheetIndex = item.parentSheetIndex + }); + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, new SObject(342, 1) + { + name = $"Pickled {item.Name}", + price = 50 + item.Price * 2, + preserve = SObject.PreserveType.Pickle, + preservedParentSheetIndex = item.parentSheetIndex + }); + } + + // flower honey + else if (item.category == SObject.flowersCategory) + { + // get honey type + SObject.HoneyType? type = null; + switch (item.parentSheetIndex) + { + case 376: + type = SObject.HoneyType.Poppy; + break; + case 591: + type = SObject.HoneyType.Tulip; + break; + case 593: + type = SObject.HoneyType.SummerSpangle; + break; + case 595: + type = SObject.HoneyType.FairyRose; + break; + case 597: + type = SObject.HoneyType.BlueJazz; + break; + case 421: // sunflower standing in for all other flowers + type = SObject.HoneyType.Wild; + break; + } + + // yield honey + if (type != null) + { + SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) + { + name = "Wild Honey", + honeyType = type + }; + if (type != SObject.HoneyType.Wild) + { + honey.name = $"{item.Name} Honey"; + honey.price += item.price * 2; + } + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, honey); + } + } + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs b/src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ac15ec72 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SMAPI.Mods.ConsoleCommands")] +[assembly: AssemblyDescription("")] +[assembly: Guid("76791e28-b1b5-407c-82d6-50c3e5b7e037")] diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj new file mode 100644 index 00000000..437d0986 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -0,0 +1,101 @@ + + + + + Debug + x86 + {28480467-1A48-46A7-99F8-236D95225359} + Library + Properties + StardewModdingAPI.Mods.ConsoleCommands + ConsoleCommands + v4.5 + 512 + + + true + full + true + $(SolutionDir)\..\bin\Debug\Mods\ConsoleCommands\ + DEBUG;TRACE + prompt + 4 + x86 + false + true + + + pdbonly + true + $(SolutionDir)\..\bin\Release\Mods\ConsoleCommands\ + TRACE + prompt + 4 + false + true + x86 + + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} + StardewModdingAPI + False + + + + + PreserveNewest + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json new file mode 100644 index 00000000..664dfabf --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -0,0 +1,13 @@ +{ + "Name": "Console Commands", + "Author": "SMAPI", + "Version": { + "MajorVersion": 2, + "MinorVersion": 0, + "PatchVersion": 0, + "Build": null + }, + "Description": "Adds SMAPI console commands that let you manipulate the game.", + "UniqueID": "SMAPI.ConsoleCommands", + "EntryDll": "ConsoleCommands.dll" +} diff --git a/src/SMAPI.Mods.ConsoleCommands/packages.config b/src/SMAPI.Mods.ConsoleCommands/packages.config new file mode 100644 index 00000000..ee51c237 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 89a8d45c..8d730f37 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.16 +VisualStudioVersion = 15.0.27004.2002 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleCommands", "SMAPI.Mods.ConsoleCommands\StardewModdingAPI.Mods.ConsoleCommands.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" EndProject diff --git a/src/TrainerMod/Framework/Commands/ArgumentParser.cs b/src/TrainerMod/Framework/Commands/ArgumentParser.cs deleted file mode 100644 index 6bcd3ff8..00000000 --- a/src/TrainerMod/Framework/Commands/ArgumentParser.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI; - -namespace TrainerMod.Framework.Commands -{ - /// Provides methods for parsing command-line arguments. - internal class ArgumentParser : IReadOnlyList - { - /********* - ** Properties - *********/ - /// The command name for errors. - private readonly string CommandName; - - /// The arguments to parse. - private readonly string[] Args; - - /// Writes messages to the console and log file. - private readonly IMonitor Monitor; - - - /********* - ** Accessors - *********/ - /// Get the number of arguments. - public int Count => this.Args.Length; - - /// Get the argument at the specified index in the list. - /// The zero-based index of the element to get. - public string this[int index] => this.Args[index]; - - /// A method which parses a string argument into the given value. - /// The expected argument type. - /// The argument to parse. - /// The parsed value. - /// Returns whether the argument was successfully parsed. - public delegate bool ParseDelegate(string input, out T output); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The command name for errors. - /// The arguments to parse. - /// Writes messages to the console and log file. - public ArgumentParser(string commandName, string[] args, IMonitor monitor) - { - this.CommandName = commandName; - this.Args = args; - this.Monitor = monitor; - } - - /// Try to read a string argument. - /// The argument index. - /// The argument name for error messages. - /// The parsed value. - /// Whether to show an error if the argument is missing. - /// Require that the argument match one of the given values (case-insensitive). - public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null) - { - value = null; - - // validate - if (this.Args.Length < index + 1) - { - if (required) - this.LogError($"Argument {index} ({name}) is required."); - return false; - } - if (oneOf?.Any() == true && !oneOf.Contains(this.Args[index], StringComparer.InvariantCultureIgnoreCase)) - { - this.LogError($"Argument {index} ({name}) must be one of {string.Join(", ", oneOf)}."); - return false; - } - - // get value - value = this.Args[index]; - return true; - } - - /// Try to read an integer argument. - /// The argument index. - /// The argument name for error messages. - /// The parsed value. - /// Whether to show an error if the argument is missing. - /// The minimum value allowed. - /// The maximum value allowed. - public bool TryGetInt(int index, string name, out int value, bool required = true, int? min = null, int? max = null) - { - value = 0; - - // get argument - if (!this.TryGet(index, name, out string raw, required)) - return false; - - // parse - if (!int.TryParse(raw, out value)) - { - this.LogIntFormatError(index, name, min, max); - return false; - } - - // validate - if ((min.HasValue && value < min) || (max.HasValue && value > max)) - { - this.LogIntFormatError(index, name, min, max); - return false; - } - - return true; - } - - /// Returns an enumerator that iterates through the collection. - /// An enumerator that can be used to iterate through the collection. - public IEnumerator GetEnumerator() - { - return ((IEnumerable)this.Args).GetEnumerator(); - } - - /// Returns an enumerator that iterates through a collection. - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - - /********* - ** Private methods - *********/ - /// Log a usage error. - /// The message describing the error. - private void LogError(string message) - { - this.Monitor.Log($"{message} Type 'help {this.CommandName}' for usage.", LogLevel.Error); - } - - /// Print an error for an invalid int argument. - /// The argument index. - /// The argument name for error messages. - /// The minimum value allowed. - /// The maximum value allowed. - private void LogIntFormatError(int index, string name, int? min, int? max) - { - if (min.HasValue && max.HasValue) - this.LogError($"Argument {index} ({name}) must be an integer between {min} and {max}."); - else if (min.HasValue) - this.LogError($"Argument {index} ({name}) must be an integer and at least {min}."); - else if (max.HasValue) - this.LogError($"Argument {index} ({name}) must be an integer and at most {max}."); - else - this.LogError($"Argument {index} ({name}) must be an integer."); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/ITrainerCommand.cs b/src/TrainerMod/Framework/Commands/ITrainerCommand.cs deleted file mode 100644 index 3d97e799..00000000 --- a/src/TrainerMod/Framework/Commands/ITrainerCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using StardewModdingAPI; - -namespace TrainerMod.Framework.Commands -{ - /// A TrainerMod command to register. - internal interface ITrainerCommand - { - /********* - ** Accessors - *********/ - /// The command name the user must type. - string Name { get; } - - /// The command description. - string Description { get; } - - /// Whether the command needs to perform logic when the game updates. - bool NeedsUpdate { get; } - - - /********* - ** Public methods - *********/ - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - void Handle(IMonitor monitor, string command, ArgumentParser args); - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - void Update(IMonitor monitor); - } -} diff --git a/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs b/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs deleted file mode 100644 index 8c6e9f3b..00000000 --- a/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs +++ /dev/null @@ -1,33 +0,0 @@ -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Other -{ - /// A command which sends a debug command to the game. - internal class DebugCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public DebugCommand() - : base("debug", "Run one of the game's debug commands; for example, 'debug warp FarmHouse 1 1' warps the player to the farmhouse.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // submit command - string debugCommand = string.Join(" ", args); - string oldOutput = Game1.debugOutput; - Game1.game1.parseDebugInput(debugCommand); - - // show result - monitor.Log(Game1.debugOutput != oldOutput - ? $"> {Game1.debugOutput}" - : "Sent debug command to the game, but there was no output.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs deleted file mode 100644 index 367a70c6..00000000 --- a/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Diagnostics; -using StardewModdingAPI; - -namespace TrainerMod.Framework.Commands.Other -{ - /// A command which shows the data files. - internal class ShowDataFilesCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public ShowDataFilesCommand() - : base("show_data_files", "Opens the folder containing the save and log files.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - Process.Start(Constants.DataPath); - monitor.Log($"OK, opening {Constants.DataPath}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs deleted file mode 100644 index 67fa83a3..00000000 --- a/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Diagnostics; -using StardewModdingAPI; - -namespace TrainerMod.Framework.Commands.Other -{ - /// A command which shows the game files. - internal class ShowGameFilesCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public ShowGameFilesCommand() - : base("show_game_files", "Opens the game folder.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - Process.Start(Constants.ExecutionPath); - monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/AddCommand.cs b/src/TrainerMod/Framework/Commands/Player/AddCommand.cs deleted file mode 100644 index 47840202..00000000 --- a/src/TrainerMod/Framework/Commands/Player/AddCommand.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Linq; -using StardewModdingAPI; -using StardewValley; -using TrainerMod.Framework.ItemData; -using Object = StardewValley.Object; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which adds an item to the player inventory. - internal class AddCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new ItemRepository(); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public AddCommand() - : base("player_add", AddCommand.GetDescription()) - { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // read arguments - if (!args.TryGet(0, "item type", out string rawType, oneOf: Enum.GetNames(typeof(ItemType)))) - return; - if (!args.TryGetInt(1, "item ID", out int id, min: 0)) - return; - if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) - count = 1; - if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) - quality = Object.lowQuality; - ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true); - - // find matching item - SearchableItem match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); - if (match == null) - { - monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); - return; - } - - // apply count & quality - match.Item.Stack = count; - if (match.Item is Object obj) - obj.quality = quality; - - // add to inventory - Game1.player.addItemByMenuIfNecessary(match.Item); - monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); - } - - /********* - ** Private methods - *********/ - private static string GetDescription() - { - string[] typeValues = Enum.GetNames(typeof(ItemType)); - return "Gives the player an item.\n" - + "\n" - + "Usage: player_add [count] [quality]\n" - + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" - + "- item: the item ID (use the 'list_items' command to see a list).\n" - + "- count (optional): how many of the item to give.\n" - + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" - + "\n" - + "This example adds the galaxy sword to your inventory:\n" - + " player_add weapon 4"; - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs b/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs deleted file mode 100644 index 5f14edbb..00000000 --- a/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using TrainerMod.Framework.ItemData; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which list item types. - internal class ListItemTypesCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new ItemRepository(); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ListItemTypesCommand() - : base("list_item_types", "Lists item types you can filter in other commands.\n\nUsage: list_item_types") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!Context.IsWorldReady) - { - monitor.Log("You need to load a save to use this command.", LogLevel.Error); - return; - } - - // handle - ItemType[] matches = - ( - from item in this.Items.GetAll() - orderby item.Type.ToString() - select item.Type - ) - .Distinct() - .ToArray(); - string summary = "Searching...\n"; - if (matches.Any()) - monitor.Log(summary + this.GetTableString(matches, new[] { "type" }, val => new[] { val.ToString() }), LogLevel.Info); - else - monitor.Log(summary + "No item types found.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs b/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs deleted file mode 100644 index 7f4f454c..00000000 --- a/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI; -using TrainerMod.Framework.ItemData; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which list items available to spawn. - internal class ListItemsCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new ItemRepository(); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ListItemsCommand() - : base("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!Context.IsWorldReady) - { - monitor.Log("You need to load a save to use this command.", LogLevel.Error); - return; - } - - // handle - SearchableItem[] matches = - ( - from item in this.GetItems(args.ToArray()) - orderby item.Type.ToString(), item.Name - select item - ) - .ToArray(); - string summary = "Searching...\n"; - if (matches.Any()) - monitor.Log(summary + this.GetTableString(matches, new[] { "type", "name", "id" }, val => new[] { val.Type.ToString(), val.Name, val.ID.ToString() }), LogLevel.Info); - else - monitor.Log(summary + "No items found", LogLevel.Info); - } - - - /********* - ** Private methods - *********/ - /// Get all items which can be searched and added to the player's inventory through the console. - /// The search string to find. - private IEnumerable GetItems(string[] searchWords) - { - // normalise search term - searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); - if (searchWords?.Any() == false) - searchWords = null; - - // find matches - return ( - from item in this.Items.GetAll() - let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}" - where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1) - select item - ); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs deleted file mode 100644 index 28ace0df..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.Xna.Framework; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the color of a player feature. - internal class SetColorCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetColorCommand() - : base("player_changecolor", "Sets the color of a player feature.\n\nUsage: player_changecolor \n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- color: a color value in RGB format, like (255,255,255).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse arguments - if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "eyes", "pants" })) - return; - if (!args.TryGet(1, "color", out string rawColor)) - return; - - // parse color - if (!this.TryParseColor(rawColor, out Color color)) - { - this.LogUsageError(monitor, "Argument 1 (color) must be an RBG value like '255,150,0'."); - return; - } - - // handle - switch (target) - { - case "hair": - Game1.player.hairstyleColor = color; - monitor.Log("OK, your hair color is updated.", LogLevel.Info); - break; - - case "eyes": - Game1.player.changeEyeColor(color); - monitor.Log("OK, your eye color is updated.", LogLevel.Info); - break; - - case "pants": - Game1.player.pantsColor = color; - monitor.Log("OK, your pants color is updated.", LogLevel.Info); - break; - } - } - - - /********* - ** Private methods - *********/ - /// Try to parse a color from a string. - /// The input string. - /// The color to set. - private bool TryParseColor(string input, out Color color) - { - string[] colorHexes = input.Split(new[] { ',' }, 3); - if (int.TryParse(colorHexes[0], out int r) && int.TryParse(colorHexes[1], out int g) && int.TryParse(colorHexes[2], out int b)) - { - color = new Color(r, g, b); - return true; - } - - color = Color.Transparent; - return false; - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs deleted file mode 100644 index f64e9035..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's current health. - internal class SetHealthCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// Whether to keep the player's health at its maximum. - private bool InfiniteHealth; - - - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteHealth; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // no-argument mode - if (!args.Any()) - { - monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - string amountStr = args[0]; - if (amountStr == "inf") - { - this.InfiniteHealth = true; - monitor.Log("OK, you now have infinite health.", LogLevel.Info); - } - else - { - this.InfiniteHealth = false; - if (int.TryParse(amountStr, out int amount)) - { - Game1.player.health = amount; - monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info); - } - else - this.LogArgumentNotInt(monitor); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) - { - if (this.InfiniteHealth) - Game1.player.health = Game1.player.maxHealth; - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs deleted file mode 100644 index 59b28a3c..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetImmunityCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's current immunity. - internal class SetImmunityCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetImmunityCommand() - : base("player_setimmunity", "Sets the player's immunity.\n\nUsage: player_setimmunity [value]\n- value: an integer amount.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.Any()) - { - monitor.Log($"You currently have {Game1.player.immunity} immunity. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - if (args.TryGetInt(0, "amount", out int amount, min: 0)) - { - Game1.player.immunity = amount; - monitor.Log($"OK, you now have {Game1.player.immunity} immunity.", LogLevel.Info); - } - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs deleted file mode 100644 index 54d5e47b..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetLevelCommand.cs +++ /dev/null @@ -1,92 +0,0 @@ -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 - *********/ - /// Construct an instance. - public SetLevelCommand() - : base("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel \n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.TryGet(0, "skill", out string skill, oneOf: new[] { "luck", "mining", "combat", "farming", "fishing", "foraging" })) - return; - if (!args.TryGetInt(1, "level", out int level, min: 0, max: 10)) - return; - - // handle - switch (skill) - { - case "luck": - Game1.player.LuckLevel = level; - Game1.player.experiencePoints[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; - } - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs deleted file mode 100644 index 4b9d87dc..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetMaxHealthCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's maximum health. - internal class SetMaxHealthCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetMaxHealthCommand() - : base("player_setmaxhealth", "Sets the player's max health.\n\nUsage: player_setmaxhealth [value]\n- value: an integer amount.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.Any()) - { - monitor.Log($"You currently have {Game1.player.maxHealth} max health. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - if (args.TryGetInt(0, "amount", out int amount, min: 1)) - { - Game1.player.maxHealth = amount; - monitor.Log($"OK, you now have {Game1.player.maxHealth} max health.", LogLevel.Info); - } - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs deleted file mode 100644 index 3997bb1b..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetMaxStaminaCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's maximum stamina. - internal class SetMaxStaminaCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetMaxStaminaCommand() - : base("player_setmaxstamina", "Sets the player's max stamina.\n\nUsage: player_setmaxstamina [value]\n- value: an integer amount.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.Any()) - { - monitor.Log($"You currently have {Game1.player.MaxStamina} max stamina. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - if (args.TryGetInt(0, "amount", out int amount, min: 1)) - { - Game1.player.MaxStamina = amount; - monitor.Log($"OK, you now have {Game1.player.MaxStamina} max stamina.", LogLevel.Info); - } - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs deleted file mode 100644 index 55e069a4..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetMoneyCommand.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's current money. - internal class SetMoneyCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// Whether to keep the player's money at a set value. - private bool InfiniteMoney; - - - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteMoney; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.Any()) - { - monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - string amountStr = args[0]; - if (amountStr == "inf") - { - this.InfiniteMoney = true; - monitor.Log("OK, you now have infinite money.", LogLevel.Info); - } - else - { - this.InfiniteMoney = false; - if (int.TryParse(amountStr, out int amount)) - { - Game1.player.Money = amount; - monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info); - } - else - this.LogArgumentNotInt(monitor); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) - { - if (this.InfiniteMoney) - Game1.player.money = 999999; - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs deleted file mode 100644 index 3fd4475c..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetNameCommand.cs +++ /dev/null @@ -1,52 +0,0 @@ -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's name. - internal class SetNameCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetNameCommand() - : base("player_setname", "Sets the player's name.\n\nUsage: player_setname \n- target: what to rename (one of 'player' or 'farm').\n- name: the new name to set.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse arguments - if (!args.TryGet(0, "target", out string target, oneOf: new[] { "player", "farm" })) - return; - args.TryGet(1, "name", out string name, required: false); - - // handle - switch (target) - { - case "player": - if (!string.IsNullOrWhiteSpace(name)) - { - Game1.player.Name = args[1]; - monitor.Log($"OK, your name is now {Game1.player.Name}.", LogLevel.Info); - } - else - monitor.Log($"Your name is currently '{Game1.player.Name}'. Type 'help player_setname' for usage.", LogLevel.Info); - break; - - case "farm": - if (!string.IsNullOrWhiteSpace(name)) - { - Game1.player.farmName = args[1]; - monitor.Log($"OK, your farm's name is now {Game1.player.farmName}.", LogLevel.Info); - } - else - monitor.Log($"Your farm's name is currently '{Game1.player.farmName}'. Type 'help player_setname' for usage.", LogLevel.Info); - break; - } - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs deleted file mode 100644 index 40b87b62..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetSpeedCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's current added speed. - internal class SetSpeedCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetSpeedCommand() - : base("player_setspeed", "Sets the player's added speed to the specified value.\n\nUsage: player_setspeed \n- value: an integer amount (0 is normal).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse arguments - if (!args.TryGetInt(0, "added speed", out int amount, min: 0)) - return; - - // handle - Game1.player.addedSpeed = amount; - monitor.Log($"OK, your added speed is now {Game1.player.addedSpeed}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs deleted file mode 100644 index d44d1370..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetStaminaCommand.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits the player's current stamina. - internal class SetStaminaCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// Whether to keep the player's stamina at its maximum. - private bool InfiniteStamina; - - - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteStamina; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.Any()) - { - monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - string amountStr = args[0]; - if (amountStr == "inf") - { - this.InfiniteStamina = true; - monitor.Log("OK, you now have infinite stamina.", LogLevel.Info); - } - else - { - this.InfiniteStamina = false; - if (int.TryParse(amountStr, out int amount)) - { - Game1.player.Stamina = amount; - monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info); - } - else - this.LogArgumentNotInt(monitor); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) - { - if (this.InfiniteStamina) - Game1.player.stamina = Game1.player.MaxStamina; - } - } -} diff --git a/src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs deleted file mode 100644 index 96e34af2..00000000 --- a/src/TrainerMod/Framework/Commands/Player/SetStyleCommand.cs +++ /dev/null @@ -1,92 +0,0 @@ -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Player -{ - /// A command which edits a player style. - internal class SetStyleCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetStyleCommand() - : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse arguments - if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" })) - return; - if (!args.TryGetInt(1, "style ID", out int styleID)) - return; - - // handle - switch (target) - { - case "hair": - Game1.player.changeHairStyle(styleID); - monitor.Log("OK, your hair style is updated.", LogLevel.Info); - break; - - case "shirt": - Game1.player.changeShirt(styleID); - monitor.Log("OK, your shirt style is updated.", LogLevel.Info); - break; - - case "acc": - Game1.player.changeAccessory(styleID); - monitor.Log("OK, your accessory style is updated.", LogLevel.Info); - break; - - case "skin": - Game1.player.changeSkinColor(styleID); - monitor.Log("OK, your skin color is updated.", LogLevel.Info); - break; - - case "shoe": - Game1.player.changeShoeColor(styleID); - monitor.Log("OK, your shoe style is updated.", LogLevel.Info); - break; - - case "swim": - switch (styleID) - { - case 0: - Game1.player.changeOutOfSwimSuit(); - monitor.Log("OK, you're no longer in your swimming suit.", LogLevel.Info); - break; - case 1: - Game1.player.changeIntoSwimsuit(); - monitor.Log("OK, you're now in your swimming suit.", LogLevel.Info); - break; - default: - this.LogUsageError(monitor, "The swim value should be 0 (no swimming suit) or 1 (swimming suit)."); - break; - } - break; - - case "gender": - switch (styleID) - { - case 0: - Game1.player.changeGender(true); - monitor.Log("OK, you're now male.", LogLevel.Info); - break; - case 1: - Game1.player.changeGender(false); - monitor.Log("OK, you're now female.", LogLevel.Info); - break; - default: - this.LogUsageError(monitor, "The gender value should be 0 (male) or 1 (female)."); - break; - } - break; - } - } - } -} diff --git a/src/TrainerMod/Framework/Commands/TrainerCommand.cs b/src/TrainerMod/Framework/Commands/TrainerCommand.cs deleted file mode 100644 index abe9ee41..00000000 --- a/src/TrainerMod/Framework/Commands/TrainerCommand.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI; - -namespace TrainerMod.Framework.Commands -{ - /// The base implementation for a trainer command. - internal abstract class TrainerCommand : ITrainerCommand - { - /********* - ** Accessors - *********/ - /// The command name the user must type. - public string Name { get; } - - /// The command description. - public string Description { get; } - - /// Whether the command needs to perform logic when the game updates. - public virtual bool NeedsUpdate { get; } = false; - - - /********* - ** Public methods - *********/ - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public abstract void Handle(IMonitor monitor, string command, ArgumentParser args); - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public virtual void Update(IMonitor monitor) { } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// The command name the user must type. - /// The command description. - protected TrainerCommand(string name, string description) - { - this.Name = name; - this.Description = description; - } - - /// Log an error indicating incorrect usage. - /// Writes messages to the console and log file. - /// A sentence explaining the problem. - protected void LogUsageError(IMonitor monitor, string error) - { - monitor.Log($"{error} Type 'help {this.Name}' for usage.", LogLevel.Error); - } - - /// Log an error indicating a value must be an integer. - /// Writes messages to the console and log file. - protected void LogArgumentNotInt(IMonitor monitor) - { - this.LogUsageError(monitor, "The value must be a whole number."); - } - - /// Get an ASCII table to show tabular data in the console. - /// The data type. - /// The data to display. - /// The table header. - /// Returns a set of fields for a data value. - protected string GetTableString(IEnumerable data, string[] header, Func getRow) - { - // get table data - int[] widths = header.Select(p => p.Length).ToArray(); - string[][] rows = data - .Select(item => - { - string[] fields = getRow(item); - if (fields.Length != widths.Length) - throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); - - for (int i = 0; i < fields.Length; i++) - widths[i] = Math.Max(widths[i], fields[i].Length); - - return fields; - }) - .ToArray(); - - // render fields - List lines = new List(rows.Length + 2) - { - header, - header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() - }; - lines.AddRange(rows); - - return string.Join( - Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) - ) - ); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs b/src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs deleted file mode 100644 index 4e62cf77..00000000 --- a/src/TrainerMod/Framework/Commands/World/DownMineLevelCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -using StardewModdingAPI; -using StardewValley; -using StardewValley.Locations; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which moves the player to the next mine level. - internal class DownMineLevelCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public DownMineLevelCommand() - : base("world_downminelevel", "Goes down one mine level.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - int level = (Game1.currentLocation as MineShaft)?.mineLevel ?? 0; - monitor.Log($"OK, warping you to mine level {level + 1}.", LogLevel.Info); - Game1.enterMine(false, level + 1, ""); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs b/src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs deleted file mode 100644 index 13d08398..00000000 --- a/src/TrainerMod/Framework/Commands/World/FreezeTimeCommand.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which freezes the current time. - internal class FreezeTimeCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// The time of day at which to freeze time. - internal static int FrozenTime; - - /// Whether to freeze time. - private bool FreezeTime; - - - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.FreezeTime; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public FreezeTimeCommand() - : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - if (args.Any()) - { - // parse arguments - if (!args.TryGetInt(0, "value", out int value, min: 0, max: 1)) - return; - - // handle - this.FreezeTime = value == 1; - FreezeTimeCommand.FrozenTime = Game1.timeOfDay; - monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info); - } - else - { - this.FreezeTime = !this.FreezeTime; - FreezeTimeCommand.FrozenTime = Game1.timeOfDay; - monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) - { - if (this.FreezeTime) - Game1.timeOfDay = FreezeTimeCommand.FrozenTime; - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/SetDayCommand.cs b/src/TrainerMod/Framework/Commands/World/SetDayCommand.cs deleted file mode 100644 index 54267384..00000000 --- a/src/TrainerMod/Framework/Commands/World/SetDayCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which sets the current day. - internal class SetDayCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetDayCommand() - : base("world_setday", "Sets the day to the specified value.\n\nUsage: world_setday .\n- value: the target day (a number from 1 to 28).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // no-argument mode - if (!args.Any()) - { - monitor.Log($"The current date is {Game1.currentSeason} {Game1.dayOfMonth}. Specify a value to change the day.", LogLevel.Info); - return; - } - - // parse arguments - if (!args.TryGetInt(0, "day", out int day, min: 1, max: 28)) - return; - - // handle - Game1.dayOfMonth = day; - monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs b/src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs deleted file mode 100644 index 225ec091..00000000 --- a/src/TrainerMod/Framework/Commands/World/SetMineLevelCommand.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which moves the player to the given mine level. - internal class SetMineLevelCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetMineLevelCommand() - : base("world_setminelevel", "Sets the mine level?\n\nUsage: world_setminelevel \n- value: The target level (a number starting at 1).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse arguments - if (!args.TryGetInt(0, "mine level", out int level, min: 1)) - return; - - // handle - level = Math.Max(1, level); - monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info); - Game1.enterMine(true, level, ""); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs b/src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs deleted file mode 100644 index 96c3d920..00000000 --- a/src/TrainerMod/Framework/Commands/World/SetSeasonCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which sets the current season. - internal class SetSeasonCommand : TrainerCommand - { - /********* - ** Properties - *********/ - /// The valid season names. - private readonly string[] ValidSeasons = { "winter", "spring", "summer", "fall" }; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetSeasonCommand() - : base("world_setseason", "Sets the season to the specified value.\n\nUsage: world_setseason \n- season: the target season (one of 'spring', 'summer', 'fall', 'winter').") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // no-argument mode - if (!args.Any()) - { - monitor.Log($"The current season is {Game1.currentSeason}. Specify a value to change it.", LogLevel.Info); - return; - } - - // parse arguments - if (!args.TryGet(0, "season", out string season, oneOf: this.ValidSeasons)) - return; - - // handle - Game1.currentSeason = season; - monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs b/src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs deleted file mode 100644 index c827ea5e..00000000 --- a/src/TrainerMod/Framework/Commands/World/SetTimeCommand.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which sets the current time. - internal class SetTimeCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetTimeCommand() - : base("world_settime", "Sets the time to the specified value.\n\nUsage: world_settime \n- value: the target time in military time (like 0600 for 6am and 1800 for 6pm).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // no-argument mode - if (!args.Any()) - { - monitor.Log($"The current time is {Game1.timeOfDay}. Specify a value to change it.", LogLevel.Info); - return; - } - - // parse arguments - if (!args.TryGetInt(0, "time", out int time, min: 600, max: 2600)) - return; - - // handle - Game1.timeOfDay = time; - FreezeTimeCommand.FrozenTime = Game1.timeOfDay; - monitor.Log($"OK, the time is now {Game1.timeOfDay.ToString().PadLeft(4, '0')}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/Commands/World/SetYearCommand.cs b/src/TrainerMod/Framework/Commands/World/SetYearCommand.cs deleted file mode 100644 index 760fc170..00000000 --- a/src/TrainerMod/Framework/Commands/World/SetYearCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.World -{ - /// A command which sets the current year. - internal class SetYearCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetYearCommand() - : base("world_setyear", "Sets the year to the specified value.\n\nUsage: world_setyear \n- year: the target year (a number starting from 1).") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // no-argument mode - if (!args.Any()) - { - monitor.Log($"The current year is {Game1.year}. Specify a value to change the year.", LogLevel.Info); - return; - } - - // parse arguments - if (!args.TryGetInt(0, "year", out int year, min: 1)) - return; - - // handle - Game1.year = year; - monitor.Log($"OK, the year is now {Game1.year}.", LogLevel.Info); - } - } -} diff --git a/src/TrainerMod/Framework/ItemData/ItemType.cs b/src/TrainerMod/Framework/ItemData/ItemType.cs deleted file mode 100644 index 423455e9..00000000 --- a/src/TrainerMod/Framework/ItemData/ItemType.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace TrainerMod.Framework.ItemData -{ - /// An item type that can be searched and added to the player through the console. - internal enum ItemType - { - /// A big craftable object in - BigCraftable, - - /// A item. - Boots, - - /// A fish item. - Fish, - - /// A flooring item. - Flooring, - - /// A item. - Furniture, - - /// A item. - Hat, - - /// Any object in (except rings). - Object, - - /// A item. - Ring, - - /// A tool. - Tool, - - /// A wall item. - Wallpaper, - - /// A or item. - Weapon - } -} diff --git a/src/TrainerMod/Framework/ItemData/SearchableItem.cs b/src/TrainerMod/Framework/ItemData/SearchableItem.cs deleted file mode 100644 index 146da1a8..00000000 --- a/src/TrainerMod/Framework/ItemData/SearchableItem.cs +++ /dev/null @@ -1,41 +0,0 @@ -using StardewValley; - -namespace TrainerMod.Framework.ItemData -{ - /// A game item with metadata. - internal class SearchableItem - { - /********* - ** Accessors - *********/ - /// The item type. - public ItemType Type { get; } - - /// The item instance. - public Item Item { get; } - - /// The item's unique ID for its type. - public int ID { get; } - - /// The item's default name. - public string Name => this.Item.Name; - - /// The item's display name for the current language. - public string DisplayName => this.Item.DisplayName; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The item type. - /// The unique ID (if different from the item's parent sheet index). - /// The item instance. - public SearchableItem(ItemType type, int id, Item item) - { - this.Type = type; - this.ID = id; - this.Item = item; - } - } -} diff --git a/src/TrainerMod/Framework/ItemRepository.cs b/src/TrainerMod/Framework/ItemRepository.cs deleted file mode 100644 index 96d3159e..00000000 --- a/src/TrainerMod/Framework/ItemRepository.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Xna.Framework; -using StardewValley; -using StardewValley.Objects; -using StardewValley.Tools; -using TrainerMod.Framework.ItemData; -using SObject = StardewValley.Object; - -namespace TrainerMod.Framework -{ - /// Provides methods for searching and constructing items. - internal class ItemRepository - { - /********* - ** Properties - *********/ - /// The custom ID offset for items don't have a unique ID in the game. - private readonly int CustomIDOffset = 1000; - - - /********* - ** Public methods - *********/ - /// Get all spawnable items. - public IEnumerable GetAll() - { - // get tools - for (int quality = Tool.stone; quality <= Tool.iridium; quality++) - { - yield return new SearchableItem(ItemType.Tool, ToolFactory.axe, ToolFactory.getToolFromDescription(ToolFactory.axe, quality)); - yield return new SearchableItem(ItemType.Tool, ToolFactory.hoe, ToolFactory.getToolFromDescription(ToolFactory.hoe, quality)); - yield return new SearchableItem(ItemType.Tool, ToolFactory.pickAxe, ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality)); - yield return new SearchableItem(ItemType.Tool, ToolFactory.wateringCan, ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality)); - if (quality != Tool.iridium) - yield return new SearchableItem(ItemType.Tool, ToolFactory.fishingRod, ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality)); - } - yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset, new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones - yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 1, new Shears()); - yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 2, new Pan()); - - // wallpapers - for (int id = 0; id < 112; id++) - yield return new SearchableItem(ItemType.Wallpaper, id, new Wallpaper(id)); - - // flooring - for (int id = 0; id < 40; id++) - yield return new SearchableItem(ItemType.Flooring, id, new Wallpaper(id, isFloor: true)); - - // equipment - foreach (int id in Game1.content.Load>("Data\\Boots").Keys) - yield return new SearchableItem(ItemType.Boots, id, new Boots(id)); - foreach (int id in Game1.content.Load>("Data\\hats").Keys) - yield return new SearchableItem(ItemType.Hat, id, new Hat(id)); - foreach (int id in Game1.objectInformation.Keys) - { - if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange) - yield return new SearchableItem(ItemType.Ring, id, new Ring(id)); - } - - // weapons - foreach (int id in Game1.content.Load>("Data\\weapons").Keys) - { - Item weapon = (id >= 32 && id <= 34) - ? (Item)new Slingshot(id) - : new MeleeWeapon(id); - yield return new SearchableItem(ItemType.Weapon, id, weapon); - } - - // furniture - foreach (int id in Game1.content.Load>("Data\\Furniture").Keys) - { - if (id == 1466 || id == 1468) - yield return new SearchableItem(ItemType.Furniture, id, new TV(id, Vector2.Zero)); - else - yield return new SearchableItem(ItemType.Furniture, id, new Furniture(id, Vector2.Zero)); - } - - // fish - foreach (int id in Game1.content.Load>("Data\\Fish").Keys) - yield return new SearchableItem(ItemType.Fish, id, new SObject(id, 999)); - - // craftables - foreach (int id in Game1.bigCraftablesInformation.Keys) - yield return new SearchableItem(ItemType.BigCraftable, id, new SObject(Vector2.Zero, id)); - - // objects - foreach (int id in Game1.objectInformation.Keys) - { - if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange) - continue; // handled separated - - SObject item = new SObject(id, 1); - yield return new SearchableItem(ItemType.Object, id, item); - - // fruit products - if (item.category == SObject.FruitsCategory) - { - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, new SObject(348, 1) - { - name = $"{item.Name} Wine", - price = item.price * 3, - preserve = SObject.PreserveType.Wine, - preservedParentSheetIndex = item.parentSheetIndex - }); - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, new SObject(344, 1) - { - name = $"{item.Name} Jelly", - price = 50 + item.Price * 2, - preserve = SObject.PreserveType.Jelly, - preservedParentSheetIndex = item.parentSheetIndex - }); - } - - // vegetable products - else if (item.category == SObject.VegetableCategory) - { - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, new SObject(350, 1) - { - name = $"{item.Name} Juice", - price = (int)(item.price * 2.25d), - preserve = SObject.PreserveType.Juice, - preservedParentSheetIndex = item.parentSheetIndex - }); - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, new SObject(342, 1) - { - name = $"Pickled {item.Name}", - price = 50 + item.Price * 2, - preserve = SObject.PreserveType.Pickle, - preservedParentSheetIndex = item.parentSheetIndex - }); - } - - // flower honey - else if (item.category == SObject.flowersCategory) - { - // get honey type - SObject.HoneyType? type = null; - switch (item.parentSheetIndex) - { - case 376: - type = SObject.HoneyType.Poppy; - break; - case 591: - type = SObject.HoneyType.Tulip; - break; - case 593: - type = SObject.HoneyType.SummerSpangle; - break; - case 595: - type = SObject.HoneyType.FairyRose; - break; - case 597: - type = SObject.HoneyType.BlueJazz; - break; - case 421: // sunflower standing in for all other flowers - type = SObject.HoneyType.Wild; - break; - } - - // yield honey - if (type != null) - { - SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) - { - name = "Wild Honey", - honeyType = type - }; - if (type != SObject.HoneyType.Wild) - { - honey.name = $"{item.Name} Honey"; - honey.price += item.price * 2; - } - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, honey); - } - } - } - } - } -} diff --git a/src/TrainerMod/Properties/AssemblyInfo.cs b/src/TrainerMod/Properties/AssemblyInfo.cs deleted file mode 100644 index 0b19e78a..00000000 --- a/src/TrainerMod/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("TrainerMod")] -[assembly: AssemblyDescription("")] -[assembly: Guid("76791e28-b1b5-407c-82d6-50c3e5b7e037")] \ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs deleted file mode 100644 index 5db02cd6..00000000 --- a/src/TrainerMod/TrainerMod.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI; -using StardewModdingAPI.Events; -using TrainerMod.Framework.Commands; - -namespace TrainerMod -{ - /// The main entry point for the mod. - public class TrainerMod : Mod - { - /********* - ** Properties - *********/ - /// The commands to handle. - private ITrainerCommand[] Commands; - - - /********* - ** Public methods - *********/ - /// The mod entry point, called after the mod is first loaded. - /// Provides simplified APIs for writing mods. - public override void Entry(IModHelper helper) - { - // register commands - this.Commands = this.ScanForCommands().ToArray(); - foreach (ITrainerCommand command in this.Commands) - helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); - - // hook events - GameEvents.UpdateTick += this.GameEvents_UpdateTick; - } - - - /********* - ** Private methods - *********/ - /// The method invoked when the game updates its state. - /// The event sender. - /// The event arguments. - private void GameEvents_UpdateTick(object sender, EventArgs e) - { - if (!Context.IsWorldReady) - return; - - foreach (ITrainerCommand command in this.Commands) - { - if (command.NeedsUpdate) - command.Update(this.Monitor); - } - } - - /// Handle a TrainerMod command. - /// The command to invoke. - /// The command name specified by the user. - /// The command arguments. - private void HandleCommand(ITrainerCommand command, string commandName, string[] args) - { - ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor); - command.Handle(this.Monitor, commandName, argParser); - } - - /// Find all commands in the assembly. - private IEnumerable ScanForCommands() - { - return ( - from type in this.GetType().Assembly.GetTypes() - where !type.IsAbstract && typeof(ITrainerCommand).IsAssignableFrom(type) - select (ITrainerCommand)Activator.CreateInstance(type) - ); - } - } -} diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj deleted file mode 100644 index cb5ec47e..00000000 --- a/src/TrainerMod/TrainerMod.csproj +++ /dev/null @@ -1,101 +0,0 @@ - - - - - Debug - x86 - {28480467-1A48-46A7-99F8-236D95225359} - Library - Properties - TrainerMod - TrainerMod - v4.5 - 512 - - - true - full - true - $(SolutionDir)\..\bin\Debug\Mods\TrainerMod\ - DEBUG;TRACE - prompt - 4 - x86 - false - true - - - pdbonly - true - $(SolutionDir)\..\bin\Release\Mods\TrainerMod\ - TRACE - prompt - 4 - false - true - x86 - - - - ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} - StardewModdingAPI - False - - - - - PreserveNewest - - - - - - \ No newline at end of file diff --git a/src/TrainerMod/manifest.json b/src/TrainerMod/manifest.json deleted file mode 100644 index 22e35bce..00000000 --- a/src/TrainerMod/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Name": "Trainer Mod", - "Author": "SMAPI", - "Version": { - "MajorVersion": 2, - "MinorVersion": 0, - "PatchVersion": 0, - "Build": null - }, - "Description": "Adds SMAPI console commands that let you manipulate the game.", - "UniqueID": "SMAPI.TrainerMod", - "EntryDll": "TrainerMod.dll" -} diff --git a/src/TrainerMod/packages.config b/src/TrainerMod/packages.config deleted file mode 100644 index ee51c237..00000000 --- a/src/TrainerMod/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file -- cgit From 3d8bdacc8cb5c9d5514e052d5d4c1d5f2dbc6e9e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 03:19:21 -0400 Subject: fix ConsoleCommands mod including Json.NET DLL --- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index 437d0986..f228bb25 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -38,6 +38,7 @@ ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + False -- cgit From 65f0fa625575592639a24a9b39330e4a6b500f22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:36:31 -0400 Subject: add scaffolding for web UI (#358) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 162 +++++++++++++++++++++++++ src/SMAPI.Web/Controllers/ModsController.cs | 162 ------------------------- 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 8 files changed, 302 insertions(+), 162 deletions(-) create mode 100644 src/SMAPI.Web/Controllers/ModsApiController.cs delete mode 100644 src/SMAPI.Web/Controllers/ModsController.cs 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 (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs new file mode 100644 index 00000000..1db5b59e --- /dev/null +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides an API to perform mod update checks. + [Produces("application/json")] + [Route("api/{version:semanticVersion}/mods")] + internal class ModsApiController : Controller + { + /********* + ** Properties + *********/ + /// The mod repositories which provide mod metadata. + private readonly IDictionary Repositories; + + /// The cache in which to store mod metadata. + private readonly IMemoryCache Cache; + + /// The number of minutes update checks should be cached before refetching them. + private readonly int CacheMinutes; + + /// A regex which matches SMAPI-style semantic version. + private readonly string VersionRegex; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cache in which to store mod metadata. + /// The config settings for mod update checks. + public ModsApiController(IMemoryCache cache, IOptions configProvider) + { + ModUpdateCheckConfig config = configProvider.Value; + + this.Cache = cache; + this.CacheMinutes = config.CacheMinutes; + this.VersionRegex = config.SemanticVersionRegex; + + string version = this.GetType().Assembly.GetName().Version.ToString(3); + this.Repositories = + new IModRepository[] + { + new ChucklefishRepository( + vendorKey: config.ChucklefishKey, + userAgent: string.Format(config.ChucklefishUserAgent, version), + baseUrl: config.ChucklefishBaseUrl, + modPageUrlFormat: config.ChucklefishModPageUrlFormat + ), + new GitHubRepository( + vendorKey: config.GitHubKey, + baseUrl: config.GitHubBaseUrl, + releaseUrlFormat: config.GitHubReleaseUrlFormat, + userAgent: string.Format(config.GitHubUserAgent, version), + acceptHeader: config.GitHubAcceptHeader, + username: config.GitHubUsername, + password: config.GitHubPassword + ), + new NexusRepository( + vendorKey: config.NexusKey, + userAgent: config.NexusUserAgent, + baseUrl: config.NexusBaseUrl, + modUrlFormat: config.NexusModUrlFormat + ) + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + } + + /// Fetch version metadata for the given mods. + /// The namespaced mod keys to search as a comma-delimited array. + [HttpGet] + public async Task> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// Fetch version metadata for the given mods. + /// The mod search criteria. + [HttpPost] + public async Task> PostAsync([FromBody] ModSearchModel search) + { + // sort & filter keys + string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) + .ToArray(); + + // fetch mod info + IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (string modKey in modKeys) + { + // parse mod key + if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) + { + result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + continue; + } + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + { + result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + continue; + } + + // fetch mod info + result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + + ModInfoModel info = await repository.GetModInfoAsync(modID); + if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) + info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + + return info; + }); + } + + return result; + } + + + /********* + ** Private methods + *********/ + /// Parse a namespaced mod ID. + /// The raw mod ID to parse. + /// The parsed vendor key. + /// The parsed mod ID. + /// Returns whether the value could be parsed. + private bool TryParseModKey(string raw, out string vendorKey, out string modID) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + { + vendorKey = null; + modID = null; + return false; + } + + // parse + vendorKey = parts[0].Trim(); + modID = parts[1].Trim(); + return true; + } + } +} diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs deleted file mode 100644 index a671ddca..00000000 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using StardewModdingAPI.Common.Models; -using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.ModRepositories; - -namespace StardewModdingAPI.Web.Controllers -{ - /// Provides an API to perform mod update checks. - [Produces("application/json")] - [Route("api/{version:semanticVersion}/[controller]")] - internal class ModsController : Controller - { - /********* - ** Properties - *********/ - /// The mod repositories which provide mod metadata. - private readonly IDictionary Repositories; - - /// The cache in which to store mod metadata. - private readonly IMemoryCache Cache; - - /// The number of minutes update checks should be cached before refetching them. - private readonly int CacheMinutes; - - /// A regex which matches SMAPI-style semantic version. - private readonly string VersionRegex; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The cache in which to store mod metadata. - /// The config settings for mod update checks. - public ModsController(IMemoryCache cache, IOptions configProvider) - { - ModUpdateCheckConfig config = configProvider.Value; - - this.Cache = cache; - this.CacheMinutes = config.CacheMinutes; - this.VersionRegex = config.SemanticVersionRegex; - - string version = this.GetType().Assembly.GetName().Version.ToString(3); - this.Repositories = - new IModRepository[] - { - new ChucklefishRepository( - vendorKey: config.ChucklefishKey, - userAgent: string.Format(config.ChucklefishUserAgent, version), - baseUrl: config.ChucklefishBaseUrl, - modPageUrlFormat: config.ChucklefishModPageUrlFormat - ), - new GitHubRepository( - vendorKey: config.GitHubKey, - baseUrl: config.GitHubBaseUrl, - releaseUrlFormat: config.GitHubReleaseUrlFormat, - userAgent: string.Format(config.GitHubUserAgent, version), - acceptHeader: config.GitHubAcceptHeader, - username: config.GitHubUsername, - password: config.GitHubPassword - ), - new NexusRepository( - vendorKey: config.NexusKey, - userAgent: config.NexusUserAgent, - baseUrl: config.NexusBaseUrl, - modUrlFormat: config.NexusModUrlFormat - ) - } - .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); - } - - /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - [HttpGet] - public async Task> GetAsync(string modKeys) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. - [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) - { - // sort & filter keys - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) - { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); - continue; - } - - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) - { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } - - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - - ModInfoModel info = await repository.GetModInfoAsync(modID); - if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) - info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); - - return info; - }); - } - - return result; - } - - - /********* - ** Private methods - *********/ - /// Parse a namespaced mod ID. - /// The raw mod ID to parse. - /// The parsed vendor key. - /// The parsed mod ID. - /// Returns whether the value could be parsed. - private bool TryParseModKey(string raw, out string vendorKey, out string modID) - { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - { - vendorKey = null; - modID = null; - return false; - } - - // parse - vendorKey = parts[0].Trim(); - modID = parts[1].Trim(); - return true; - } - } -} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index eaf14983..abce8f28 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -64,6 +64,7 @@ namespace StardewModdingAPI.Web loggerFactory.AddDebug(); app .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseStaticFiles() // wwwroot folder .UseMvc(); } } diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000..89b1866c --- /dev/null +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -0,0 +1,29 @@ + + + + + @ViewData["Title"] - SMAPI.io + + + + +
+
+

@ViewData["Title"]

+ @RenderBody() +
+ +
+ + diff --git a/src/SMAPI.Web/Views/_ViewStart.cshtml b/src/SMAPI.Web/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/src/SMAPI.Web/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/SMAPI.Web/wwwroot/Content/main.css b/src/SMAPI.Web/wwwroot/Content/main.css new file mode 100644 index 00000000..c8ce8d33 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/main.css @@ -0,0 +1,107 @@ +/* tags */ +html { + height: 100%; +} + +body { + height: 100%; + font-family: sans-serif; +} + +h1, h2, h3 { + font-weight: bold; + margin: 0.2em 0 0.1em 0; + padding-top: .5em; +} + +h1 { + font-size: 1.5em; + color: #888; + margin-bottom: 0; +} + +h2 { + font-size: 1.5em; + border-bottom: 1px solid #AAA; +} + +h3 { + font-size: 1.2em; + border-bottom: 1px solid #AAA; + width: 50%; +} + +a { + color: #006; +} + +/* content */ +#content-column { + position: absolute; + top: 1em; + left: 10em; +} + +#content { + min-height: 140px; + padding: 0 1em 1em 1em; + border-left: 1px solid #CCC; + background: #FFF; + font-size: 0.9em; +} + +#content p { + max-width: 55em; +} + +.section { + border: 1px solid #CCC; + padding: 0.5em; + margin-bottom: 1em; +} + +/* sidebar */ +#sidebar { + margin-top: 3em; + min-height: 75%; + width: 12em; + background: url("sidebar-bg.gif") no-repeat top right; + color: #666; +} + +#sidebar h4 { + margin: 0 0 0.2em 0; + width: 10em; + border-bottom: 1px solid #CCC; + font-size: 0.8em; + font-weight: normal; +} + +#sidebar a { + color: #77B; + border: 0; +} + +#sidebar ul, #sidebar li { + margin: 0; + padding: 0; + list-style: none none; + font-size: 0.9em; + color: #888; +} + +#sidebar li { + margin-left: 1em; +} + +/* footer */ +#footer { + margin: 1em; + padding: 1em; + font-size: 0.6em; + color: gray; +} + +#footer a { + color: #669; +} diff --git a/src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif b/src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif new file mode 100644 index 00000000..48e9af5a Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif differ diff --git a/src/SMAPI.Web/wwwroot/favicon.ico b/src/SMAPI.Web/wwwroot/favicon.ico new file mode 100644 index 00000000..587a6e74 Binary files /dev/null and b/src/SMAPI.Web/wwwroot/favicon.ico differ -- cgit From e75aef8634f9edb8ac385b8d7308b40ed3269cbc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:36:52 -0400 Subject: add placeholder for new log parser (#358) --- src/SMAPI.Web/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 (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs new file mode 100644 index 00000000..4ed8898a --- /dev/null +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides a web UI and API for parsing SMAPI log files. + [Route("log")] + internal class LogParserController : Controller + { + /********* + ** Public methods + *********/ + /// Render the web UI to upload a log file. + [HttpGet] + public ViewResult Index() + { + return this.View("Index"); + } + } +} diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml new file mode 100644 index 00000000..cd47d687 --- /dev/null +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -0,0 +1,14 @@ +@{ + ViewData["Title"] = "SMAPI log parser"; +} + +

How to share a SMAPI log

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

Parsed log

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

Parsed log

TODO + + + + + + + SMAPI log parser + + + + +
    +
  • TRACE
  • +
  • DEBUG
  • +
  • INFO
  • +
  • ALERT
  • +
  • WARN
  • +
  • ERROR
  • +
  • Click tabs to toggle message visibility
  • +
  • UPLOAD
  • +
+
+ + + + + + + +
+ + + + + -- cgit From 6cbe43a233eccbc6c8d1cfdd9c80e391463eb7c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:37:49 -0400 Subject: use CDN for jQuery and lz-string (#358) --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 830cfe47..417fe428 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -486,9 +486,8 @@
- + + - -- cgit From 9f5af37391ac196fe183122f57496846843335cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:38:13 -0400 Subject: 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 +- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 374 ++++++++++++ src/SMAPI.Web/wwwroot/Content/css/main.css | 107 ++++ .../wwwroot/Content/images/sidebar-bg.gif | Bin 0 -> 1104 bytes src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 287 +++++++++ src/SMAPI.Web/wwwroot/Content/main.css | 107 ---- src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif | Bin 1104 -> 0 bytes 8 files changed, 776 insertions(+), 776 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/Content/css/log-parser.css create mode 100644 src/SMAPI.Web/wwwroot/Content/css/main.css create mode 100644 src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif create mode 100644 src/SMAPI.Web/wwwroot/Content/js/log-parser.js delete mode 100644 src/SMAPI.Web/wwwroot/Content/main.css delete mode 100644 src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 417fe428..021293b6 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,6 +1,12 @@ @{ ViewData["Title"] = "SMAPI log parser"; } +@section Head { + + + + +}

How to share a SMAPI log

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

    Unable to save!

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

    Parsing failed!

    Parsing of the log failed, details follow.
     

    Stage: Upload

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

    Parsing failed!

    Parsing of the log failed, details follow.
     

    Stage: Upload

    Error: Received null response
    ' + $("#input").val() + '
    '); + }) + } else { + alert("Unable to parse log, the input is empty!"); + $("#uploader").fadeOut(); + } + }) + $("#cancel").on("click", function() { + $("#popup-upload").fadeOut(400, function() { + $("#input").val(memory); + memory = ""; + }); + }); + $("#closeraw").on("click", function() { + $("#popup-raw").fadeOut(400); + }) + if (location.search) { + getData(); + } + else + $("#popup-upload").fadeIn(); +}) diff --git a/src/SMAPI.Web/wwwroot/Content/main.css b/src/SMAPI.Web/wwwroot/Content/main.css deleted file mode 100644 index c8ce8d33..00000000 --- a/src/SMAPI.Web/wwwroot/Content/main.css +++ /dev/null @@ -1,107 +0,0 @@ -/* 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 deleted file mode 100644 index 48e9af5a..00000000 Binary files a/src/SMAPI.Web/wwwroot/Content/sidebar-bg.gif and /dev/null differ -- cgit From 467b9aa2df8532aa3cb94c84307c7012573d61d4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:38:37 -0400 Subject: integrate prototype into page layout (#358) --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 197 +++++++++++------------ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 77 +++------ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 2 +- 3 files changed, 122 insertions(+), 154 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 021293b6..87a3962b 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -6,114 +6,107 @@ + } -

    How to share a SMAPI log

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

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

    +

    Parsed log

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