diff options
40 files changed, 1119 insertions, 423 deletions
diff --git a/build/0Harmony.dll b/build/0Harmony.dll Binary files differindex 91d36ea2..492255be 100644 --- a/build/0Harmony.dll +++ b/build/0Harmony.dll diff --git a/build/0Harmony.xml b/build/0Harmony.xml index ba2f340e..8499d20b 100644 --- a/build/0Harmony.xml +++ b/build/0Harmony.xml @@ -265,6 +265,9 @@ <member name="F:HarmonyLib.MethodType.StaticConstructor"> <summary>This is a static constructor</summary> </member> + <member name="F:HarmonyLib.MethodType.Enumerator"> + <summary>This targets the MoveNext method of the enumerator result</summary> + </member> <member name="T:HarmonyLib.ArgumentType"> <summary>Specifies the type of argument</summary> @@ -476,6 +479,13 @@ <param name="argumentVariations">An array of <see cref="T:HarmonyLib.ArgumentType"/></param> </member> + <member name="M:HarmonyLib.HarmonyPatch.#ctor(System.String,System.String,HarmonyLib.MethodType)"> + <summary>An annotation that specifies a method, property or constructor to patch</summary> + <param name="typeName">The full name of the declaring class/type</param> + <param name="methodName">The name of the method, property or constructor to patch</param> + <param name="methodType">The <see cref="T:HarmonyLib.MethodType"/></param> + + </member> <member name="T:HarmonyLib.HarmonyDelegate"> <summary>Annotation to define the original method for delegate injection</summary> @@ -797,6 +807,13 @@ <returns></returns> </member> + <member name="M:HarmonyLib.CodeInstruction.CallClosure``1(``0)"> + <summary>Returns an instruction to call the specified closure</summary> + <typeparam name="T">The delegate type to emit</typeparam> + <param name="closure">The closure that defines the method to call</param> + <returns>A <see cref="T:HarmonyLib.CodeInstruction"/> that calls the closure as a method</returns> + + </member> <member name="M:HarmonyLib.CodeInstruction.LoadField(System.Type,System.String,System.Boolean)"> <summary>Creates a CodeInstruction loading a field (LD[S]FLD[A])</summary> <param name="type">The class/type where the field is defined</param> @@ -980,6 +997,11 @@ <returns>For normal frames, <c>frame.GetMethod()</c> is returned. For frames containing patched methods, the replacement method is returned or <c>null</c> if no method can be found</returns> </member> + <member name="M:HarmonyLib.Harmony.GetOriginalMethodFromStackframe(System.Diagnostics.StackFrame)"> + <summary>Gets the original method from the stackframe and uses original if method is a dynamic replacement</summary> + <param name="frame">The <see cref="T:System.Diagnostics.StackFrame"/></param> + <returns>The original method from that stackframe</returns> + </member> <member name="M:HarmonyLib.Harmony.VersionInfo(System.Version@)"> <summary>Gets Harmony version for all active Harmony instances</summary> <param name="currentVersion">[out] The current Harmony version</param> @@ -1210,7 +1232,7 @@ </member> <member name="T:HarmonyLib.PatchInfoSerialization"> - <summary>Patch serialization</summary> + <summary>Patch serialization</summary> </member> <member name="M:HarmonyLib.PatchInfoSerialization.Binder.BindToType(System.String,System.String)"> @@ -1241,27 +1263,27 @@ </member> <member name="T:HarmonyLib.PatchInfo"> - <summary>Serializable patch information</summary> + <summary>Serializable patch information</summary> </member> <member name="F:HarmonyLib.PatchInfo.prefixes"> - <summary>Prefixes as an array of <see cref="T:HarmonyLib.Patch"/></summary> + <summary>Prefixes as an array of <see cref="T:HarmonyLib.Patch"/></summary> </member> <member name="F:HarmonyLib.PatchInfo.postfixes"> - <summary>Postfixes as an array of <see cref="T:HarmonyLib.Patch"/></summary> + <summary>Postfixes as an array of <see cref="T:HarmonyLib.Patch"/></summary> </member> <member name="F:HarmonyLib.PatchInfo.transpilers"> - <summary>Transpilers as an array of <see cref="T:HarmonyLib.Patch"/></summary> + <summary>Transpilers as an array of <see cref="T:HarmonyLib.Patch"/></summary> </member> <member name="F:HarmonyLib.PatchInfo.finalizers"> - <summary>Finalizers as an array of <see cref="T:HarmonyLib.Patch"/></summary> + <summary>Finalizers as an array of <see cref="T:HarmonyLib.Patch"/></summary> </member> <member name="P:HarmonyLib.PatchInfo.Debugging"> - <summary>Returns if any of the patches wants debugging turned on</summary> + <summary>Returns if any of the patches wants debugging turned on</summary> </member> <member name="M:HarmonyLib.PatchInfo.AddPrefixes(System.String,HarmonyLib.HarmonyMethod[])"> @@ -1339,35 +1361,35 @@ </member> <member name="T:HarmonyLib.Patch"> - <summary>A serializable patch</summary> + <summary>A serializable patch</summary> </member> <member name="F:HarmonyLib.Patch.index"> - <summary>Zero-based index</summary> + <summary>Zero-based index</summary> </member> <member name="F:HarmonyLib.Patch.owner"> - <summary>The owner (Harmony ID)</summary> + <summary>The owner (Harmony ID)</summary> </member> <member name="F:HarmonyLib.Patch.priority"> - <summary>The priority, see <see cref="T:HarmonyLib.Priority"/></summary> + <summary>The priority, see <see cref="T:HarmonyLib.Priority"/></summary> </member> <member name="F:HarmonyLib.Patch.before"> - <summary>Keep this patch before the patches indicated in the list of Harmony IDs</summary> + <summary>Keep this patch before the patches indicated in the list of Harmony IDs</summary> </member> <member name="F:HarmonyLib.Patch.after"> - <summary>Keep this patch after the patches indicated in the list of Harmony IDs</summary> + <summary>Keep this patch after the patches indicated in the list of Harmony IDs</summary> </member> <member name="F:HarmonyLib.Patch.debug"> - <summary>A flag that will log the replacement method via <see cref="T:HarmonyLib.FileLog"/> every time this patch is used to build the replacement, even in the future</summary> + <summary>A flag that will log the replacement method via <see cref="T:HarmonyLib.FileLog"/> every time this patch is used to build the replacement, even in the future</summary> </member> <member name="P:HarmonyLib.Patch.PatchMethod"> - <summary>The method of the static patch method</summary> + <summary>The method of the static patch method</summary> </member> <member name="M:HarmonyLib.Patch.#ctor(System.Reflection.MethodInfo,System.Int32,System.String,System.Int32,System.String[],System.String[],System.Boolean)"> @@ -1761,6 +1783,12 @@ <returns>A field or null when type/name is null or when the field cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.DeclaredField(System.String)"> + <summary>Gets the reflection information for a directly declared field</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A field or null when the field cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.Field(System.Type,System.String)"> <summary>Gets the reflection information for a field by searching the type and all its super types</summary> <param name="type">The class/type where the field is defined</param> @@ -1768,6 +1796,12 @@ <returns>A field or null when type/name is null or when the field cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.Field(System.String)"> + <summary>Gets the reflection information for a field by searching the type and all its super types</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A field or null when the field cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.DeclaredField(System.Type,System.Int32)"> <summary>Gets the reflection information for a field</summary> <param name="type">The class/type where the field is declared</param> @@ -1782,6 +1816,12 @@ <returns>A property or null when type/name is null or when the property cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.DeclaredProperty(System.String)"> + <summary>Gets the reflection information for a directly declared property</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A property or null when the property cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.DeclaredPropertyGetter(System.Type,System.String)"> <summary>Gets the reflection information for the getter method of a directly declared property</summary> <param name="type">The class/type where the property is declared</param> @@ -1789,6 +1829,12 @@ <returns>A method or null when type/name is null or when the property cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.DeclaredPropertyGetter(System.String)"> + <summary>Gets the reflection information for the getter method of a directly declared property</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A method or null when the property cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.DeclaredPropertySetter(System.Type,System.String)"> <summary>Gets the reflection information for the setter method of a directly declared property</summary> <param name="type">The class/type where the property is declared</param> @@ -1796,6 +1842,12 @@ <returns>A method or null when type/name is null or when the property cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.DeclaredPropertySetter(System.String)"> + <summary>Gets the reflection information for the Setter method of a directly declared property</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A method or null when the property cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.Property(System.Type,System.String)"> <summary>Gets the reflection information for a property by searching the type and all its super types</summary> <param name="type">The class/type</param> @@ -1803,6 +1855,12 @@ <returns>A property or null when type/name is null or when the property cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.Property(System.String)"> + <summary>Gets the reflection information for a property by searching the type and all its super types</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A property or null when the property cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.PropertyGetter(System.Type,System.String)"> <summary>Gets the reflection information for the getter method of a property by searching the type and all its super types</summary> <param name="type">The class/type</param> @@ -1810,6 +1868,12 @@ <returns>A method or null when type/name is null or when the property cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.PropertyGetter(System.String)"> + <summary>Gets the reflection information for the getter method of a property by searching the type and all its super types</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A method or null when type/name is null or when the property cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.PropertySetter(System.Type,System.String)"> <summary>Gets the reflection information for the setter method of a property by searching the type and all its super types</summary> <param name="type">The class/type</param> @@ -1817,6 +1881,12 @@ <returns>A method or null when type/name is null or when the property cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.PropertySetter(System.String)"> + <summary>Gets the reflection information for the setter method of a property by searching the type and all its super types</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A method or null when type/name is null or when the property cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.DeclaredMethod(System.Type,System.String,System.Type[],System.Type[])"> <summary>Gets the reflection information for a directly declared method</summary> <param name="type">The class/type where the method is declared</param> @@ -1826,6 +1896,14 @@ <returns>A method or null when type/name is null or when the method cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.DeclaredMethod(System.String,System.Type[],System.Type[])"> + <summary>Gets the reflection information for a directly declared method</summary> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <param name="parameters">Optional parameters to target a specific overload of the method</param> + <param name="generics">Optional list of types that define the generic version of the method</param> + <returns>A method or null when the method cannot be found</returns> + + </member> <member name="M:HarmonyLib.AccessTools.Method(System.Type,System.String,System.Type[],System.Type[])"> <summary>Gets the reflection information for a method by searching the type and all its super types</summary> <param name="type">The class/type where the method is declared</param> @@ -1837,12 +1915,17 @@ </member> <member name="M:HarmonyLib.AccessTools.Method(System.String,System.Type[],System.Type[])"> <summary>Gets the reflection information for a method by searching the type and all its super types</summary> - <param name="typeColonMethodname">The target method in the form <c>TypeFullName:MethodName</c>, where the type name matches a form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> <param name="parameters">Optional parameters to target a specific overload of the method</param> <param name="generics">Optional list of types that define the generic version of the method</param> - <returns>A method or null when type/name is null or when the method cannot be found</returns> + <returns>A method or null when the method cannot be found</returns> </member> + <member name="M:HarmonyLib.AccessTools.EnumeratorMoveNext(System.Reflection.MethodBase)"> + <summary>Gets the <see cref="M:System.Collections.IEnumerator.MoveNext" /> method of an enumerator method</summary> + <param name="method">Enumerator method that creates the enumerator <see cref="T:System.Collections.IEnumerator" /></param> + <returns>The internal <see cref="M:System.Collections.IEnumerator.MoveNext" /> method of the enumerator or <b>null</b> if no valid enumerator is detected</returns> + </member> <member name="M:HarmonyLib.AccessTools.GetMethodNames(System.Type)"> <summary>Gets the names of all method that are declared in a type</summary> <param name="type">The declaring class/type</param> @@ -2109,6 +2192,12 @@ </remarks> </member> + <member name="M:HarmonyLib.AccessTools.FieldRefAccess``1(System.String)"> + <summary>Creates a field reference delegate for an instance field of a class or static field (NOT an instance field of a struct)</summary> + <typeparam name="F"> type of the field</typeparam> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A readable/assignable <see cref="T:HarmonyLib.AccessTools.FieldRef`2"/> delegate with <c>T=object</c></returns> + </member> <member name="M:HarmonyLib.AccessTools.FieldRefAccess``2(System.Reflection.FieldInfo)"> <summary>Creates a field reference delegate for an instance field of a class or static field (NOT an instance field of a struct)</summary> <typeparam name="T"> @@ -2282,6 +2371,13 @@ <returns>A readable/assignable reference to the field</returns> </member> + <member name="M:HarmonyLib.AccessTools.StaticFieldRefAccess``1(System.String)"> + <summary>Creates a static field reference</summary> + <typeparam name="F">The type of the field</typeparam> + <param name="typeColonName">The member in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <returns>A readable/assignable reference to the field</returns> + + </member> <member name="M:HarmonyLib.AccessTools.StaticFieldRefAccess``2(System.Reflection.FieldInfo)"> <summary>Creates a static field reference</summary> <typeparam name="T">An arbitrary type (by convention, the type the field is defined in)</typeparam> @@ -2337,6 +2433,34 @@ </remarks> </member> + <member name="M:HarmonyLib.AccessTools.MethodDelegate``1(System.String,System.Object,System.Boolean)"> + <summary>Creates a delegate to a given method</summary> + <typeparam name="DelegateType">The delegate Type</typeparam> + <param name="typeColonName">The method in the form <c>TypeFullName:MemberName</c>, where TypeFullName matches the form recognized by <a href="https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype">Type.GetType</a> like <c>Some.Namespace.Type</c>.</param> + <param name="instance"> + Only applies for instance methods. If <c>null</c> (default), returned delegate is an open (a.k.a. unbound) instance delegate + where an instance is supplied as the first argument to the delegate invocation; else, delegate is a closed (a.k.a. bound) + instance delegate where the delegate invocation always applies to the given <paramref name="instance"/>. + </param> + <param name="virtualCall"> + Only applies for instance methods. If <c>true</c> (default) and <paramref name="typeColonName"/> is virtual, invocation of the delegate + calls the instance method virtually (the instance type's most-derived/overriden implementation of the method is called); + else, invocation of the delegate calls the exact specified <paramref name="typeColonName"/> (this is useful for calling base class methods) + Note: if <c>false</c> and <paramref name="typeColonName"/> is an interface method, an ArgumentException is thrown. + </param> + <returns>A delegate of given <typeparamref name="DelegateType"/> to given <paramref name="typeColonName"/></returns> + <remarks> + <para> + Delegate invocation is more performant and more convenient to use than <see cref="M:System.Reflection.MethodBase.Invoke(System.Object,System.Object[])"/> + at a one-time setup cost. + </para> + <para> + Works for both type of static and instance methods, both open and closed (a.k.a. unbound and bound) instance methods, + and both class and struct methods. + </para> + </remarks> + + </member> <member name="M:HarmonyLib.AccessTools.HarmonyDelegate``1(System.Object)"> <summary>Creates a delegate for a given delegate definition, attributed with [<see cref="T:HarmonyLib.HarmonyDelegate"/>]</summary> <typeparam name="DelegateType">The delegate Type, attributed with [<see cref="T:HarmonyLib.HarmonyDelegate"/>]</typeparam> @@ -2509,6 +2633,412 @@ <returns>The hash code</returns> </member> + <member name="T:HarmonyLib.CodeMatch"> + <summary>A CodeInstruction match</summary> + </member> + <member name="F:HarmonyLib.CodeMatch.name"> + <summary>The name of the match</summary> + </member> + <member name="F:HarmonyLib.CodeMatch.opcodes"> + <summary>The matched opcodes</summary> + </member> + <member name="F:HarmonyLib.CodeMatch.operands"> + <summary>The matched operands</summary> + </member> + <member name="F:HarmonyLib.CodeMatch.jumpsFrom"> + <summary>The jumps from the match</summary> + </member> + <member name="F:HarmonyLib.CodeMatch.jumpsTo"> + <summary>The jumps to the match</summary> + </member> + <member name="F:HarmonyLib.CodeMatch.predicate"> + <summary>The match predicate</summary> + </member> + <member name="M:HarmonyLib.CodeMatch.#ctor(System.Nullable{System.Reflection.Emit.OpCode},System.Object,System.String)"> + <summary>Creates a code match</summary> + <param name="opcode">The optional opcode</param> + <param name="operand">The optional operand</param> + <param name="name">The optional name</param> + + </member> + <member name="M:HarmonyLib.CodeMatch.#ctor(HarmonyLib.CodeInstruction,System.String)"> + <summary>Creates a code match</summary> + <param name="instruction">The CodeInstruction</param> + <param name="name">An optional name</param> + + </member> + <member name="M:HarmonyLib.CodeMatch.#ctor(System.Func{HarmonyLib.CodeInstruction,System.Boolean},System.String)"> + <summary>Creates a code match</summary> + <param name="predicate">The predicate</param> + <param name="name">An optional name</param> + + </member> + <member name="M:HarmonyLib.CodeMatch.ToString"> + <summary>Returns a string that represents the match</summary> + <returns>A string representation</returns> + + </member> + <member name="T:HarmonyLib.CodeMatcher"> + <summary>A CodeInstruction matcher</summary> + </member> + <member name="P:HarmonyLib.CodeMatcher.Pos"> + <summary>The current position</summary> + <value>The index or -1 if out of bounds</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Length"> + <summary>Gets the number of code instructions in this matcher</summary> + <value>The count</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.IsValid"> + <summary>Checks whether the position of this CodeMatcher is within bounds</summary> + <value>True if this CodeMatcher is valid</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.IsInvalid"> + <summary>Checks whether the position of this CodeMatcher is outside its bounds</summary> + <value>True if this CodeMatcher is invalid</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Remaining"> + <summary>Gets the remaining code instructions</summary> + <value>The remaining count</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Opcode"> + <summary>Gets the opcode at the current position</summary> + <value>The opcode</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Operand"> + <summary>Gets the operand at the current position</summary> + <value>The operand</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Labels"> + <summary>Gets the labels at the current position</summary> + <value>The labels</value> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Blocks"> + <summary>Gets the exception blocks at the current position</summary> + <value>The blocks</value> + + </member> + <member name="M:HarmonyLib.CodeMatcher.#ctor"> + <summary>Creates an empty code matcher</summary> + </member> + <member name="M:HarmonyLib.CodeMatcher.#ctor(System.Collections.Generic.IEnumerable{HarmonyLib.CodeInstruction},System.Reflection.Emit.ILGenerator)"> + <summary>Creates a code matcher from an enumeration of instructions</summary> + <param name="instructions">The instructions (transpiler argument)</param> + <param name="generator">An optional IL generator</param> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Clone"> + <summary>Makes a clone of this instruction matcher</summary> + <returns>A copy of this matcher</returns> + + </member> + <member name="P:HarmonyLib.CodeMatcher.Instruction"> + <summary>Gets instructions at the current position</summary> + <value>The instruction</value> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InstructionAt(System.Int32)"> + <summary>Gets instructions at the current position with offset</summary> + <param name="offset">The offset</param> + <returns>The instruction</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Instructions"> + <summary>Gets all instructions</summary> + <returns>A list of instructions</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InstructionEnumeration"> + <summary>Gets all instructions as an enumeration</summary> + <returns>A list of instructions</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Instructions(System.Int32)"> + <summary>Gets some instructions counting from current position</summary> + <param name="count">Number of instructions</param> + <returns>A list of instructions</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InstructionsInRange(System.Int32,System.Int32)"> + <summary>Gets all instructions within a range</summary> + <param name="start">The start index</param> + <param name="end">The end index</param> + <returns>A list of instructions</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InstructionsWithOffsets(System.Int32,System.Int32)"> + <summary>Gets all instructions within a range (relative to current position)</summary> + <param name="startOffset">The start offset</param> + <param name="endOffset">The end offset</param> + <returns>A list of instructions</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.DistinctLabels(System.Collections.Generic.IEnumerable{HarmonyLib.CodeInstruction})"> + <summary>Gets a list of all distinct labels</summary> + <param name="instructions">The instructions (transpiler argument)</param> + <returns>A list of Labels</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.ReportFailure(System.Reflection.MethodBase,System.Action{System.String})"> + <summary>Reports a failure</summary> + <param name="method">The method involved</param> + <param name="logger">The logger</param> + <returns>True if current position is invalid and error was logged</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.ThrowIfInvalid(System.String)"> + <summary>Throw an InvalidOperationException if current state is invalid (position out of bounds / last match failed)</summary> + <param name="explanation">Explanation of where/why the exception was thrown that will be added to the exception message</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.ThrowIfNotMatch(System.String,HarmonyLib.CodeMatch[])"> + <summary>Throw an InvalidOperationException if current state is invalid (position out of bounds / last match failed), + or if the matches do not match at current position</summary> + <param name="explanation">Explanation of where/why the exception was thrown that will be added to the exception message</param> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.ThrowIfNotMatchForward(System.String,HarmonyLib.CodeMatch[])"> + <summary>Throw an InvalidOperationException if current state is invalid (position out of bounds / last match failed), + or if the matches do not match at any point between current position and the end</summary> + <param name="explanation">Explanation of where/why the exception was thrown that will be added to the exception message</param> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.ThrowIfNotMatchBack(System.String,HarmonyLib.CodeMatch[])"> + <summary>Throw an InvalidOperationException if current state is invalid (position out of bounds / last match failed), + or if the matches do not match at any point between current position and the start</summary> + <param name="explanation">Explanation of where/why the exception was thrown that will be added to the exception message</param> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.ThrowIfFalse(System.String,System.Func{HarmonyLib.CodeMatcher,System.Boolean})"> + <summary>Throw an InvalidOperationException if current state is invalid (position out of bounds / last match failed), + or if the check function returns false</summary> + <param name="explanation">Explanation of where/why the exception was thrown that will be added to the exception message</param> + <param name="stateCheckFunc">Function that checks validity of current state. If it returns false, an exception is thrown</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SetInstruction(HarmonyLib.CodeInstruction)"> + <summary>Sets an instruction at current position</summary> + <param name="instruction">The instruction to set</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SetInstructionAndAdvance(HarmonyLib.CodeInstruction)"> + <summary>Sets instruction at current position and advances</summary> + <param name="instruction">The instruction</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Set(System.Reflection.Emit.OpCode,System.Object)"> + <summary>Sets opcode and operand at current position</summary> + <param name="opcode">The opcode</param> + <param name="operand">The operand</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SetAndAdvance(System.Reflection.Emit.OpCode,System.Object)"> + <summary>Sets opcode and operand at current position and advances</summary> + <param name="opcode">The opcode</param> + <param name="operand">The operand</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SetOpcodeAndAdvance(System.Reflection.Emit.OpCode)"> + <summary>Sets opcode at current position and advances</summary> + <param name="opcode">The opcode</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SetOperandAndAdvance(System.Object)"> + <summary>Sets operand at current position and advances</summary> + <param name="operand">The operand</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.CreateLabel(System.Reflection.Emit.Label@)"> + <summary>Creates a label at current position</summary> + <param name="label">[out] The label</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.CreateLabelAt(System.Int32,System.Reflection.Emit.Label@)"> + <summary>Creates a label at a position</summary> + <param name="position">The position</param> + <param name="label">[out] The new label</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.CreateLabelWithOffsets(System.Int32,System.Reflection.Emit.Label@)"> + <summary>Creates a label at a position</summary> + <param name="offset">The offset</param> + <param name="label">[out] The new label</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.AddLabels(System.Collections.Generic.IEnumerable{System.Reflection.Emit.Label})"> + <summary>Adds an enumeration of labels to current position</summary> + <param name="labels">The labels</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.AddLabelsAt(System.Int32,System.Collections.Generic.IEnumerable{System.Reflection.Emit.Label})"> + <summary>Adds an enumeration of labels at a position</summary> + <param name="position">The position</param> + <param name="labels">The labels</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SetJumpTo(System.Reflection.Emit.OpCode,System.Int32,System.Reflection.Emit.Label@)"> + <summary>Sets jump to</summary> + <param name="opcode">Branch instruction</param> + <param name="destination">Destination for the jump</param> + <param name="label">[out] The created label</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Insert(HarmonyLib.CodeInstruction[])"> + <summary>Inserts some instructions</summary> + <param name="instructions">The instructions</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Insert(System.Collections.Generic.IEnumerable{HarmonyLib.CodeInstruction})"> + <summary>Inserts an enumeration of instructions</summary> + <param name="instructions">The instructions</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InsertBranch(System.Reflection.Emit.OpCode,System.Int32)"> + <summary>Inserts a branch</summary> + <param name="opcode">The branch opcode</param> + <param name="destination">Branch destination</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InsertAndAdvance(HarmonyLib.CodeInstruction[])"> + <summary>Inserts some instructions and advances the position</summary> + <param name="instructions">The instructions</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InsertAndAdvance(System.Collections.Generic.IEnumerable{HarmonyLib.CodeInstruction})"> + <summary>Inserts an enumeration of instructions and advances the position</summary> + <param name="instructions">The instructions</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.InsertBranchAndAdvance(System.Reflection.Emit.OpCode,System.Int32)"> + <summary>Inserts a branch and advances the position</summary> + <param name="opcode">The branch opcode</param> + <param name="destination">Branch destination</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.RemoveInstruction"> + <summary>Removes current instruction</summary> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.RemoveInstructions(System.Int32)"> + <summary>Removes some instruction from current position by count</summary> + <param name="count">Number of instructions</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.RemoveInstructionsInRange(System.Int32,System.Int32)"> + <summary>Removes the instructions in a range</summary> + <param name="start">The start</param> + <param name="end">The end</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.RemoveInstructionsWithOffsets(System.Int32,System.Int32)"> + <summary>Removes the instructions in a offset range</summary> + <param name="startOffset">The start offset</param> + <param name="endOffset">The end offset</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Advance(System.Int32)"> + <summary>Advances the current position</summary> + <param name="offset">The offset</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Start"> + <summary>Moves the current position to the start</summary> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.End"> + <summary>Moves the current position to the end</summary> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SearchForward(System.Func{HarmonyLib.CodeInstruction,System.Boolean})"> + <summary>Searches forward with a predicate and advances position</summary> + <param name="predicate">The predicate</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.SearchBackwards(System.Func{HarmonyLib.CodeInstruction,System.Boolean})"> + <summary>Searches backwards with a predicate and reverses position</summary> + <param name="predicate">The predicate</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.MatchStartForward(HarmonyLib.CodeMatch[])"> + <summary>Matches forward and advances position to beginning of matching sequence</summary> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.MatchEndForward(HarmonyLib.CodeMatch[])"> + <summary>Matches forward and advances position to ending of matching sequence</summary> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.MatchStartBackwards(HarmonyLib.CodeMatch[])"> + <summary>Matches backwards and reverses position to beginning of matching sequence</summary> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.MatchEndBackwards(HarmonyLib.CodeMatch[])"> + <summary>Matches backwards and reverses position to ending of matching sequence</summary> + <param name="matches">Some code matches</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.Repeat(System.Action{HarmonyLib.CodeMatcher},System.Action{System.String})"> + <summary>Repeats a match action until boundaries are met</summary> + <param name="matchAction">The match action</param> + <param name="notFoundAction">An optional action that is executed when no match is found</param> + <returns>The same code matcher</returns> + + </member> + <member name="M:HarmonyLib.CodeMatcher.NamedMatch(System.String)"> + <summary>Gets a match by its name</summary> + <param name="name">The match name</param> + <returns>An instruction</returns> + + </member> <member name="T:HarmonyLib.GeneralExtensions"> <summary>General extensions for common cases</summary> @@ -2574,6 +3104,11 @@ <summary>Extensions for <see cref="T:HarmonyLib.CodeInstruction"/></summary> </member> + <member name="M:HarmonyLib.CodeInstructionExtensions.IsValid(System.Reflection.Emit.OpCode)"> + <summary>Returns if an <see cref="T:System.Reflection.Emit.OpCode"/> is initialized and valid</summary> + <param name="code">The <see cref="T:System.Reflection.Emit.OpCode"/></param> + <returns></returns> + </member> <member name="M:HarmonyLib.CodeInstructionExtensions.OperandIs(HarmonyLib.CodeInstruction,System.Object)"> <summary>Shortcut for testing whether the operand is equal to a non-null value</summary> <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/></param> @@ -2715,15 +3250,15 @@ <returns>A list of <see cref="T:System.Reflection.Emit.Label"/></returns> </member> <member name="M:HarmonyLib.CodeInstructionExtensions.MoveLabelsTo(HarmonyLib.CodeInstruction,HarmonyLib.CodeInstruction)"> - <summary>Moves all labels from the code instruction to a different one</summary> + <summary>Moves all labels from the code instruction to another one</summary> <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels from</param> - <param name="other">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels to</param> + <param name="other">The other <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels to</param> <returns>The code instruction labels were moved from (now empty)</returns> </member> <member name="M:HarmonyLib.CodeInstructionExtensions.MoveLabelsFrom(HarmonyLib.CodeInstruction,HarmonyLib.CodeInstruction)"> - <summary>Moves all labels from a different code instruction to the current one</summary> - <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels from</param> - <param name="other">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels to</param> + <summary>Moves all labels from another code instruction to the current one</summary> + <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels to</param> + <param name="other">The other <see cref="T:HarmonyLib.CodeInstruction"/> to move the labels from</param> <returns>The code instruction that received the labels</returns> </member> <member name="M:HarmonyLib.CodeInstructionExtensions.WithBlocks(HarmonyLib.CodeInstruction,HarmonyLib.ExceptionBlock[])"> @@ -2744,15 +3279,15 @@ <returns>A list of <see cref="T:HarmonyLib.ExceptionBlock"/></returns> </member> <member name="M:HarmonyLib.CodeInstructionExtensions.MoveBlocksTo(HarmonyLib.CodeInstruction,HarmonyLib.CodeInstruction)"> - <summary>Moves all ExceptionBlocks from the code instruction to a different one</summary> + <summary>Moves all ExceptionBlocks from the code instruction to another one</summary> <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks from</param> - <param name="other">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks to</param> + <param name="other">The other <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks to</param> <returns>The code instruction blocks were moved from (now empty)</returns> </member> <member name="M:HarmonyLib.CodeInstructionExtensions.MoveBlocksFrom(HarmonyLib.CodeInstruction,HarmonyLib.CodeInstruction)"> - <summary>Moves all ExceptionBlocks from a different code instruction to the current one</summary> - <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks from</param> - <param name="other">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks to</param> + <summary>Moves all ExceptionBlocks from another code instruction to the current one</summary> + <param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks to</param> + <param name="other">The other <see cref="T:HarmonyLib.CodeInstruction"/> to move the ExceptionBlocks from</param> <returns>The code instruction that received the blocks</returns> </member> <member name="T:HarmonyLib.CollectionExtensions"> @@ -2860,6 +3395,11 @@ <param name="str">The string to log.</param> </member> + <member name="M:HarmonyLib.FileLog.Debug(System.String)"> + <summary>Log a string directly to disk if Harmony.DEBUG is true. Slower method that prevents missing information in case of a crash</summary> + <param name="str">The string to log.</param> + + </member> <member name="M:HarmonyLib.FileLog.Reset"> <summary>Resets and deletes the log</summary> diff --git a/build/common.targets b/build/common.targets index ca9a1d12..8b17c45a 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,7 +1,7 @@ <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <!--set general build properties --> - <Version>3.14.7</Version> + <Version>3.15.0</Version> <Product>SMAPI</Product> <LangVersion>latest</LangVersion> <AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> diff --git a/docs/release-notes.md b/docs/release-notes.md index d8cfa350..496d016a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,11 +1,31 @@ ← [README](README.md) # Release notes +## 3.15.0 +Released 17 June 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/67877219). + +* For players: + * Optimized mod image file loading. + * Minor optimizations (thanks to Michael Kuklinski / Ameisen!). + * Updated compatibility list. + +* For mod authors: + * Added an [`IRawTextureData` asset type](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Raw_texture_data), to avoid creating full `Texture2D` instances in many cases. + * In `smapi-internal/config.json`, you can now enable verbose logging for specific mods (instead of all or nothing). + * Updated dependencies: + * Harmony 2.2.1 (see changes in [2.2.0](https://github.com/pardeike/Harmony/releases/tag/v2.2.0.0) and [2.2.1](https://github.com/pardeike/Harmony/releases/tag/v2.2.1.0)); + * Newtonsoft.Json 13.0.1 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.1)); + * Pintail 2.2.0. + * Removed transitional `UsePintail` option added in 3.14.0 (now always enabled). + * Fixed `onBehalfOf` arguments in the new content API being case-sensitive. + * Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players. + ## 3.14.7 Released 01 June 2022 for Stardew Valley 1.5.6 or later. * For players: * Optimized reflection cache to reduce frame skips for some players. + * For mod authors: * Removed `runtimeconfig.json` setting which impacted hot reload support. diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index 264932e4..3be9c225 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -6,9 +6,9 @@ <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> - <PackageReference Include="NUnit" Version="3.13.2" /> - <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> + <PackageReference Include="NUnit" Version="3.13.3" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index c5790186..e25da168 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -24,7 +24,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <!-- This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 564e480e..300de9d2 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.14.7", + "Version": "3.15.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.14.7" + "MinimumApiVersion": "3.15.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index bfbfd2dc..22e68421 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -2,6 +2,7 @@ using System; using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Internal.Patching; +using StardewModdingAPI.Mods.ErrorHandler.ModPatches; using StardewModdingAPI.Mods.ErrorHandler.Patches; using StardewValley; @@ -29,6 +30,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler // apply patches HarmonyPatcher.Apply(this.ModManifest.UniqueID, this.Monitor, + // game patches new DialoguePatcher(monitorForGame, this.Helper.Reflection), new EventPatcher(monitorForGame), new GameLocationPatcher(monitorForGame), @@ -37,7 +39,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler new ObjectPatcher(), new SaveGamePatcher(this.Monitor, this.OnSaveContentRemoved), new SpriteBatchPatcher(), - new UtilityPatcher() + new UtilityPatcher(), + + // mod patches + new PyTkPatcher(helper.ModRegistry) ); // hook events diff --git a/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs b/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs new file mode 100644 index 00000000..9ee864db --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Patching; + +// +// This is part of a three-part fix for PyTK 1.23.0 and earlier. When removing this, search +// 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher. +// + +namespace StardewModdingAPI.Mods.ErrorHandler.ModPatches +{ + /// <summary>Harmony patches for the PyTK mod for compatibility with newer SMAPI versions.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "'Platonymous' is part of the mod ID.")] + internal class PyTkPatcher : BasePatcher + { + /********* + ** Fields + *********/ + /// <summary>The PyTK mod metadata, if it's installed.</summary> + private static IModMetadata? PyTk; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="modRegistry">The mod registry from which to read PyTK metadata.</param> + public PyTkPatcher(IModRegistry modRegistry) + { + IModMetadata? pyTk = (IModMetadata?)modRegistry.Get(@"Platonymous.Toolkit"); + if (pyTk is not null && !pyTk.Manifest.Version.IsNewerThan("1.23.0")) + PyTkPatcher.PyTk = pyTk; + } + + /// <inheritdoc /> + public override void Apply(Harmony harmony, IMonitor monitor) + { + try + { + // get mod info + IModMetadata? pyTk = PyTkPatcher.PyTk; + if (pyTk is null) + return; + + // get patch method + const string patchMethodName = "PatchImage"; + MethodInfo? patch = AccessTools.Method(pyTk.Mod!.GetType(), patchMethodName); + if (patch is null) + { + monitor.Log("Failed applying compatibility patch for PyTK. Its image scaling feature may not work correctly.", LogLevel.Warn); + monitor.Log($"Couldn't find patch method '{pyTk.Mod.GetType().FullName}.{patchMethodName}'."); + return; + } + + // apply patch + harmony = new($"{harmony.Id}.compatibility-patches.PyTK"); + harmony.Patch( + original: AccessTools.Method(typeof(AssetDataForImage), nameof(AssetDataForImage.PatchImage), new[] { typeof(Texture2D), typeof(Rectangle), typeof(Rectangle), typeof(PatchMode) }), + prefix: new HarmonyMethod(patch) + ); + } + catch (Exception ex) + { + monitor.Log("Failed applying compatibility patch for PyTK. Its image scaling feature may not work correctly.", LogLevel.Warn); + monitor.Log(ex.GetLogSummary()); + } + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index 78cdb315..53c37e97 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -24,7 +24,4 @@ <None Update="i18n\*.json" CopyToOutputDirectory="PreserveNewest" /> <None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> - - <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" /> - <Import Project="..\SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems" Label="Shared" /> </Project> diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 39d22b5f..15a1e0f3 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.14.7", + "Version": "3.15.0", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.14.7" + "MinimumApiVersion": "3.15.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 8eaf2475..1a11742c 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.14.7", + "Version": "3.15.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.14.7" + "MinimumApiVersion": "3.15.0" } diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs index 6be97526..d14c116f 100644 --- a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs +++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs @@ -29,11 +29,8 @@ namespace SMAPI.Tests.Core /// <summary>The random number generator with which to create sample values.</summary> private readonly Random Random = new(); - /// <summary>Sample user inputs for season names.</summary> - private static readonly IInterfaceProxyFactory[] ProxyFactories = { - new InterfaceProxyFactory(), - new OriginalInterfaceProxyFactory() - }; + /// <summary>The proxy factory to use in unit tests.</summary> + private static readonly IInterfaceProxyFactory[] ProxyFactories = { new InterfaceProxyFactory() }; /********* diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 67997b30..2c32a932 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -14,11 +14,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.5.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> - <PackageReference Include="Moq" Version="4.16.1" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="FluentAssertions" Version="6.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> + <PackageReference Include="Moq" Version="4.18.1" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="NUnit" Version="3.13.3" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index ec27bf79..e021993f 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -9,8 +9,8 @@ <Import Project="..\..\build\common.targets" /> <ItemGroup> - <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.43" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> <PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index f1400e62..4c2569e1 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -15,14 +15,14 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Azure.Storage.Blobs" Version="12.10.0" /> - <PackageReference Include="Hangfire.AspNetCore" Version="1.7.27" /> + <PackageReference Include="Azure.Storage.Blobs" Version="12.12.0" /> + <PackageReference Include="Hangfire.AspNetCore" Version="1.7.29" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" /> - <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> - <PackageReference Include="Humanizer.Core" Version="2.13.14" /> - <PackageReference Include="JetBrains.Annotations" Version="2021.3.0" /> - <PackageReference Include="Markdig" Version="0.26.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.43" /> + <PackageReference Include="Humanizer.Core" Version="2.14.1" /> + <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> + <PackageReference Include="Markdig" Version="0.30.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.5" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.5" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 53c9db82..16a89647 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -170,6 +170,11 @@ /********* ** Broke in SMAPI 3.14.0 *********/ + "CFAutomate": { + "ID": "Platonymous.CFAutomate", + "~2.12.9 | Status": "AssumeBroken", + "~2.12.9 | StatusReasonDetails": "causes runtime errors in newer versions of Automate" + }, "Dynamic Game Assets": { "ID": "spacechase0.DynamicGameAssets", "~1.4.1 | Status": "AssumeBroken", diff --git a/src/SMAPI.sln b/src/SMAPI.sln index d9f60a5c..2e5ba0ea 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -103,14 +103,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3B5BF14D-F61 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiProvider", "SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj", "{239AEEAC-07D1-4A3F-AA99-8C74F5038F50}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMAPI.Tests.ModApiConsumer", "SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj", "{2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiConsumer", "SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj", "{2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 - SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 - SMAPI.Internal\SMAPI.Internal.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{6c16e948-3e5c-47a7-bf4b-07a7469a87a5}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index c63324e3..db88563e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI internal static int? LogScreenId { get; set; } /// <summary>SMAPI's current raw semantic version.</summary> - internal static string RawApiVersion = "3.14.7"; + internal static string RawApiVersion = "3.15.0"; } /// <summary>Contains SMAPI's constants and assumptions.</summary> diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 97729c95..3393b22f 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -29,86 +30,154 @@ namespace StardewModdingAPI.Framework.Content : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// <inheritdoc /> - public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - // get texture + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + + // validate source data if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); - Texture2D target = this.Data; + throw new ArgumentNullException(nameof(source), "Can't patch from null source data."); - // get areas - sourceArea ??= new Rectangle(0, 0, source.Width, source.Height); - targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + // get the pixels for the source area + Color[] sourceData; + { + int areaX = sourceArea.Value.X; + int areaY = sourceArea.Value.Y; + int areaWidth = sourceArea.Value.Width; + int areaHeight = sourceArea.Value.Height; - // validate + if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height) + sourceData = source.Data; + else + { + sourceData = new Color[areaWidth * areaHeight]; + int i = 0; + for (int y = areaY, maxY = areaY + areaHeight - 1; y <= maxY; y++) + { + for (int x = areaX, maxX = areaX + areaWidth - 1; x <= maxX; x++) + { + int targetIndex = (y * source.Width) + x; + sourceData[i++] = source.Data[targetIndex]; + } + } + } + } + + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + + /// <inheritdoc /> + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + + // validate source texture + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); if (!source.Bounds.Contains(sourceArea.Value)) throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (!target.Bounds.Contains(targetArea.Value)) - throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Size != targetArea.Value.Size) - throw new InvalidOperationException("The source and target areas must be the same size."); // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = GC.AllocateUninitializedArray<Color>(pixelCount); source.GetData(0, sourceArea, sourceData, 0, pixelCount); - // merge data in overlay mode + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + + /// <inheritdoc /> + public bool ExtendImage(int minWidth, int minHeight) + { + if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) + return false; + + Texture2D original = this.Data; + Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); + this.ReplaceWith(texture); + this.PatchImage(original); + return true; + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the bounds for an image patch.</summary> + /// <param name="sourceArea">The source area to set if needed.</param> + /// <param name="targetArea">The target area to set if needed.</param> + /// <param name="sourceWidth">The width of the full source image.</param> + /// <param name="sourceHeight">The height of the full source image.</param> + private void GetPatchBounds([NotNull] ref Rectangle? sourceArea, [NotNull] ref Rectangle? targetArea, int sourceWidth, int sourceHeight) + { + sourceArea ??= new Rectangle(0, 0, sourceWidth, sourceHeight); + targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, this.Data.Width), Math.Min(sourceArea.Value.Height, this.Data.Height)); + } + + /// <summary>Overwrite part of the image.</summary> + /// <param name="sourceData">The image data to patch into the content.</param> + /// <param name="sourceWidth">The pixel width of the source image.</param> + /// <param name="sourceHeight">The pixel height of the source image.</param> + /// <param name="sourceArea">The part of the <paramref name="sourceData"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="sourceData"/> texture.</param> + /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param> + /// <param name="patchMode">Indicates how an image should be patched.</param> + /// <exception cref="ArgumentNullException">One of the arguments is null.</exception> + /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> + /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception> + private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode) + { + // get texture + Texture2D target = this.Data; + int pixelCount = sourceArea.Width * sourceArea.Height; + + // validate + if (sourceArea.X < 0 || sourceArea.Y < 0 || sourceArea.Right > sourceWidth || sourceArea.Bottom > sourceHeight) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (!target.Bounds.Contains(targetArea)) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Size != targetArea.Size) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // merge data if (patchMode == PatchMode.Overlay) { // get target data - Color[] targetData = GC.AllocateUninitializedArray<Color>(pixelCount); - target.GetData(0, targetArea, targetData, 0, pixelCount); + Color[] mergedData = GC.AllocateUninitializedArray<Color>(pixelCount); + target.GetData(0, targetArea, mergedData, 0, pixelCount); // merge pixels - for (int i = 0; i < sourceData.Length; i++) + for (int i = 0; i < pixelCount; i++) { Color above = sourceData[i]; - Color below = targetData[i]; + Color below = mergedData[i]; // shortcut transparency if (above.A < MinOpacity) - { - sourceData[i] = below; continue; - } if (below.A < MinOpacity) - { - sourceData[i] = above; - continue; - } + mergedData[i] = above; // merge pixels - // This performs a conventional alpha blend for the pixels, which are already - // premultiplied by the content pipeline. The formula is derived from - // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. - // Note: don't use named arguments here since they're different between - // Linux/macOS and Windows. - float alphaBelow = 1 - (above.A / 255f); - sourceData[i] = new Color( - (int)(above.R + (below.R * alphaBelow)), // r - (int)(above.G + (below.G * alphaBelow)), // g - (int)(above.B + (below.B * alphaBelow)), // b - Math.Max(above.A, below.A) // a - ); + else + { + // This performs a conventional alpha blend for the pixels, which are already + // premultiplied by the content pipeline. The formula is derived from + // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + float alphaBelow = 1 - (above.A / 255f); + mergedData[i] = new Color( + r: (int)(above.R + (below.R * alphaBelow)), + g: (int)(above.G + (below.G * alphaBelow)), + b: (int)(above.B + (below.B * alphaBelow)), + alpha: Math.Max(above.A, below.A) + ); + } } - } - - // patch target texture - target.SetData(0, targetArea, sourceData, 0, pixelCount); - } - - /// <inheritdoc /> - public bool ExtendImage(int minWidth, int minHeight) - { - if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) - return false; - Texture2D original = this.Data; - Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); - this.ReplaceWith(texture); - this.PatchImage(original); - return true; + target.SetData(0, targetArea, mergedData, 0, pixelCount); + } + else + target.SetData(0, targetArea, sourceData, 0, pixelCount); } } } diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 736ee5da..bf42812b 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Framework.Content ** Fields *********/ /// <summary>The underlying asset cache.</summary> - private readonly IDictionary<string, object> Cache; + private readonly Dictionary<string, object> Cache; /********* @@ -29,7 +29,7 @@ namespace StardewModdingAPI.Framework.Content } /// <summary>The current cache keys.</summary> - public IEnumerable<string> Keys => this.Cache.Keys; + public Dictionary<string, object>.KeyCollection Keys => this.Cache.Keys; /********* @@ -89,33 +89,36 @@ namespace StardewModdingAPI.Framework.Content /// <returns>Returns the removed key (if any).</returns> public bool Remove(string key, bool dispose) { - // get entry - if (!this.Cache.TryGetValue(key, out object? value)) + // remove and get entry + if (!this.Cache.Remove(key, out object? value)) return false; // dispose & remove entry if (dispose && value is IDisposable disposable) disposable.Dispose(); - return this.Cache.Remove(key); + return true; } - /// <summary>Purge matched assets from the cache.</summary> + /// <summary>Purge assets matching <paramref name="predicate"/> from the cache.</summary> /// <param name="predicate">Matches the asset keys to invalidate.</param> - /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param> - /// <returns>Returns the removed keys (if any).</returns> + /// <param name="dispose">Whether to dispose invalidated assets. This should only be <see langword="true"/> when they're being invalidated as part of a <see cref="IDisposable.Dispose"/>, to avoid crashing the game.</param> + /// <returns>Returns any removed keys.</returns> public IEnumerable<string> Remove(Func<string, object, bool> predicate, bool dispose) { - List<string> removed = new List<string>(); - foreach (string key in this.Cache.Keys.ToArray()) + List<string> removed = new(); + foreach ((string key, object value) in this.Cache) { - if (predicate(key, this.Cache[key])) - { - this.Remove(key, dispose); + if (predicate(key, value)) removed.Add(key); - } } - return removed; + + foreach (string key in removed) + this.Remove(key, dispose); + + return removed.Count == 0 + ? Enumerable.Empty<string>() // let GC collect the list in gen0 instead of potentially living longer + : removed; } } } diff --git a/src/SMAPI/Framework/Content/RawTextureData.cs b/src/SMAPI/Framework/Content/RawTextureData.cs new file mode 100644 index 00000000..4a0835b0 --- /dev/null +++ b/src/SMAPI/Framework/Content/RawTextureData.cs @@ -0,0 +1,10 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Framework.Content +{ + /// <summary>The raw data for an image read from the filesystem.</summary> + /// <param name="Width">The image width.</param> + /// <param name="Height">The image height.</param> + /// <param name="Data">The loaded image data.</param> + internal record RawTextureData(int Width, int Height, Color[] Data) : IRawTextureData; +} diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index cfeb35c8..3e09ac62 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -32,6 +32,9 @@ namespace StardewModdingAPI.Framework /// <summary>An asset key prefix for assets from SMAPI mod folders.</summary> private readonly string ManagedPrefix = "SMAPI"; + /// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary> + private readonly bool UseRawImageLoading; + /// <summary>Get a file lookup for the given directory.</summary> private readonly Func<string, IFileLookup> GetFileLookup; @@ -130,7 +133,8 @@ namespace StardewModdingAPI.Framework /// <param name="getFileLookup">Get a file lookup for the given directory.</param> /// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param> /// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param> - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations) + /// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param> + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations, bool useRawImageLoading) { this.GetFileLookup = getFileLookup; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); @@ -141,6 +145,7 @@ namespace StardewModdingAPI.Framework this.OnAssetsInvalidated = onAssetsInvalidated; this.RequestAssetOperations = requestAssetOperations; this.FullRootDirectory = Path.Combine(Constants.GamePath, rootDirectory); + this.UseRawImageLoading = useRawImageLoading; this.ContentManagers.Add( this.MainContentManager = new GameContentManager( name: "Game1.content", @@ -219,7 +224,8 @@ namespace StardewModdingAPI.Framework reflection: this.Reflection, jsonHelper: this.JsonHelper, onDisposing: this.OnDisposing, - fileLookup: this.GetFileLookup(rootDirectory) + fileLookup: this.GetFileLookup(rootDirectory), + useRawImageLoading: this.UseRawImageLoading ); this.ContentManagers.Add(manager); return manager; @@ -465,7 +471,7 @@ namespace StardewModdingAPI.Framework assets: invalidatedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded, out IDictionary<IAssetName, bool> propagated, - out bool updatedNpcWarps + out bool updatedWarpRoutes ); // log summary @@ -481,8 +487,8 @@ namespace StardewModdingAPI.Framework ? $"Propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})." : "Propagated 0 core assets." ); - if (updatedNpcWarps) - report.AppendLine("Updated NPC pathfinding cache."); + if (updatedWarpRoutes) + report.AppendLine("Updated NPC warp route cache."); } this.Monitor.Log(report.ToString().TrimEnd()); } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 4390d472..446f4a67 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -9,6 +9,7 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Deprecations; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; @@ -93,6 +94,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <inheritdoc /> public override T LoadExact<T>(IAssetName assetName, bool useCache) { + if (typeof(IRawTextureData).IsAssignableFrom(typeof(T))) + throw new SContentLoadException(ContentLoadErrorType.Other, $"Can't load {nameof(IRawTextureData)} assets from the game content pipeline. This asset type is only available for mod files."); + // raise first-load callback if (GameContentManager.IsFirstLoad) { diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1b94b8c6..8c5d0f84 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -7,6 +9,8 @@ using BmFont; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using SkiaSharp; +using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialization; @@ -20,11 +24,14 @@ using xTile.Tiles; namespace StardewModdingAPI.Framework.ContentManagers { /// <summary>A content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary> - internal class ModContentManager : BaseContentManager + internal sealed class ModContentManager : BaseContentManager { /********* ** Fields *********/ + /// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary> + private readonly bool UseRawImageLoading; + /// <summary>Encapsulates SMAPI's JSON file parsing.</summary> private readonly JsonHelper JsonHelper; @@ -38,7 +45,17 @@ namespace StardewModdingAPI.Framework.ContentManagers private readonly IFileLookup FileLookup; /// <summary>If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder.</summary> - private static readonly HashSet<string> LocalTilesheetExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".xnb" }; + private static readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" }; + + /// <summary>A lookup of image file paths to whether they have PyTK scaling information.</summary> + private static readonly Dictionary<string, bool> IsPyTkScaled = new(StringComparer.OrdinalIgnoreCase); + + + /********* + ** Accessors + *********/ + /// <summary>Whether to enable legacy compatibility mode for PyTK scale-up textures.</summary> + internal static bool EnablePyTkLegacyMode; /********* @@ -57,13 +74,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param> /// <param name="fileLookup">A lookup for files within the <paramref name="rootDirectory"/>.</param> - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFileLookup fileLookup) + /// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param> + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFileLookup fileLookup, bool useRawImageLoading) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; this.JsonHelper = jsonHelper; this.ModName = modName; + this.UseRawImageLoading = useRawImageLoading; this.TryLocalizeKeys = false; } @@ -119,8 +138,11 @@ namespace StardewModdingAPI.Framework.ContentManagers _ => this.HandleUnknownFileType<T>(assetName, file) }; } - catch (Exception ex) when (ex is not SContentLoadException) + catch (Exception ex) { + if (ex is SContentLoadException) + throw; + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex); } @@ -130,6 +152,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } /// <inheritdoc /> + [Obsolete($"Temporary {nameof(ModContentManager)}s are unsupported")] public override LocalizedContentManager CreateTemporary() { throw new NotSupportedException("Can't create a temporary mod content manager."); @@ -155,11 +178,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="file">The file to load.</param> private T LoadFont<T>(IAssetName assetName, FileInfo file) { - // validate - if (!typeof(T).IsAssignableFrom(typeof(XmlSource))) - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'."); + this.AssertValidType<T>(assetName, file, typeof(XmlSource)); - // load string source = File.ReadAllText(file.FullName); return (T)(object)new XmlSource(source); } @@ -182,15 +202,89 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="file">The file to load.</param> private T LoadImageFile<T>(IAssetName assetName, FileInfo file) { - // validate - if (typeof(T) != typeof(Texture2D)) - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + this.AssertValidType<T>(assetName, file, typeof(Texture2D), typeof(IRawTextureData)); + bool expectsRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); + bool asRawData = expectsRawData || this.UseRawImageLoading; + + // disable raw data if PyTK will rescale the image (until it supports raw data) + if (asRawData && !expectsRawData) + { + if (ModContentManager.EnablePyTkLegacyMode) + { + if (!ModContentManager.IsPyTkScaled.TryGetValue(file.FullName, out bool isScaled)) + { + string? dirPath = file.DirectoryName; + string fileName = $"{Path.GetFileNameWithoutExtension(file.Name)}.pytk.json"; + + string path = dirPath is not null + ? Path.Combine(dirPath, fileName) + : fileName; + + ModContentManager.IsPyTkScaled[file.FullName] = isScaled = File.Exists(path); + } + + asRawData = !isScaled; + if (!asRawData) + this.Monitor.LogOnce("Enabled compatibility mode for PyTK scaled textures. This won't cause any issues, but may impact performance.", LogLevel.Warn); + } + else + asRawData = true; + } // load - using FileStream stream = File.OpenRead(file.FullName); - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - return (T)(object)texture; + if (asRawData) + { + IRawTextureData raw = this.LoadRawImageData(file, expectsRawData); + + if (expectsRawData) + return (T)raw; + else + { + Texture2D texture = new(Game1.graphics.GraphicsDevice, raw.Width, raw.Height); + texture.SetData(raw.Data); + return (T)(object)texture; + } + } + else + { + using FileStream stream = File.OpenRead(file.FullName); + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + return (T)(object)texture; + } + } + + /// <summary>Load the raw image data from a file on disk.</summary> + /// <param name="file">The file whose data to load.</param> + /// <param name="forRawData">Whether the data is being loaded for an <see cref="IRawTextureData"/> (true) or <see cref="Texture2D"/> (false) instance.</param> + /// <remarks>This is separate to let framework mods intercept the data before it's loaded, if needed.</remarks> + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] + private IRawTextureData LoadRawImageData(FileInfo file, bool forRawData) + { + // load raw data + int width; + int height; + SKPMColor[] rawPixels; + { + using FileStream stream = File.OpenRead(file.FullName); + using SKBitmap bitmap = SKBitmap.Decode(stream); + rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); + width = bitmap.Width; + height = bitmap.Height; + } + + // convert to XNA pixel format + var pixels = GC.AllocateUninitializedArray<Color>(rawPixels.Length); + for (int i = 0; i < pixels.Length; i++) + { + SKPMColor pixel = rawPixels[i]; + pixels[i] = pixel.Alpha == 0 + ? Color.Transparent + : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); + } + + return new RawTextureData(width, height, pixels); } /// <summary>Load an unpacked image file (<c>.tbin</c> or <c>.tmx</c>).</summary> @@ -199,11 +293,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="file">The file to load.</param> private T LoadMapFile<T>(IAssetName assetName, FileInfo file) { - // validate - if (typeof(T) != typeof(Map)) - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + this.AssertValidType<T>(assetName, file, typeof(Map)); - // load FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); map.assetPath = assetName.Name; @@ -216,6 +307,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="assetName">The asset name relative to the loader root directory.</param> private T LoadXnbFile<T>(IAssetName assetName) { + if (typeof(IRawTextureData).IsAssignableFrom(typeof(T))) + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file."); + // the underlying content manager adds a .xnb extension implicitly, so // we need to strip it here to avoid trying to load a '.xnb.xnb' file. IAssetName loadName = assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase) @@ -242,11 +336,24 @@ namespace StardewModdingAPI.Framework.ContentManagers throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); } + /// <summary>Assert that the asset type is compatible with one of the allowed types.</summary> + /// <typeparam name="TAsset">The actual asset type.</typeparam> + /// <param name="assetName">The asset name relative to the loader root directory.</param> + /// <param name="file">The file being loaded.</param> + /// <param name="validTypes">The allowed asset types.</param> + /// <exception cref="SContentLoadException">The <typeparamref name="TAsset"/> is not compatible with any of the <paramref name="validTypes"/>.</exception> + private void AssertValidType<TAsset>(IAssetName assetName, FileInfo file, params Type[] validTypes) + { + if (!validTypes.Any(validType => validType.IsAssignableFrom(typeof(TAsset)))) + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TAsset)}'; must be type '{string.Join("' or '", validTypes.Select(p => p.FullName))}'."); + } + /// <summary>Get an error which indicates that an asset couldn't be loaded.</summary> /// <param name="errorType">Why loading an asset through the content pipeline failed.</param> /// <param name="assetName">The asset name that failed to load.</param> /// <param name="reasonPhrase">The reason the file couldn't be loaded.</param> /// <param name="exception">The underlying exception, if applicable.</param> + [DebuggerStepThrough, DebuggerHidden] private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); @@ -261,7 +368,7 @@ namespace StardewModdingAPI.Framework.ContentManagers FileInfo file = this.FileLookup.GetFile(path); // try with default image extensions - if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension)) + if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension, StringComparer.OrdinalIgnoreCase)) { foreach (string extension in ModContentManager.LocalTilesheetExtensions) { @@ -284,7 +391,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private Texture2D PremultiplyTransparency(Texture2D texture) { // premultiply pixels - Color[] data = new Color[texture.Width * texture.Height]; + Color[] data = GC.AllocateUninitializedArray<Color>(texture.Width * texture.Height); texture.GetData(data); bool changed = false; for (int i = 0; i < data.Length; i++) @@ -324,7 +431,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // reverse incorrect eager tilesheet path prefixing if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource.StartsWith(relativeMapFolder)) - imageSource = imageSource.Substring(relativeMapFolder.Length + 1); + imageSource = imageSource[(relativeMapFolder.Length + 1)..]; // validate tilesheet path string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; @@ -345,8 +452,11 @@ namespace StardewModdingAPI.Framework.ContentManagers tilesheet.ImageSource = assetName.Name; } } - catch (Exception ex) when (ex is not SContentLoadException) + catch (Exception ex) { + if (ex is SContentLoadException) + throw; + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex); } } @@ -361,7 +471,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <remarks>See remarks on <see cref="FixTilesheetPaths"/>.</remarks> private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, out string? error) { - assetName = null; error = null; // nothing to do @@ -376,7 +485,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // opened in Tiled, while still mapping it to the vanilla 'Maps/spring_town' asset at runtime. { string filename = Path.GetFileName(relativePath); - if (filename.StartsWith(".")) + if (filename.StartsWith('.')) relativePath = Path.Combine(Path.GetDirectoryName(relativePath) ?? "", filename.TrimStart('.')); } @@ -391,7 +500,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // get from game assets - IAssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false); + AssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false); try { this.GameContentManager.LoadLocalized<Texture2D>(contentKey, this.GameContentManager.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset @@ -412,6 +521,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // not found + assetName = null; error = "The tilesheet couldn't be found relative to either map file or the game's content folder."; return false; } @@ -422,11 +532,11 @@ namespace StardewModdingAPI.Framework.ContentManagers { // get file path string path = Path.Combine(this.GameContentManager.FullRootDirectory, key); - if (!path.EndsWith(".xnb")) + if (!path.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)) path += ".xnb"; // get file - return new FileInfo(path).Exists; + return File.Exists(path); } /// <summary>Get the asset key for a tilesheet in the game's <c>Maps</c> content folder.</summary> @@ -442,7 +552,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // remove file extension from unpacked file if (key.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - key = key.Substring(0, key.Length - 4); + key = key[..^4]; return key; } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index ed5b6959..d811ed5c 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -31,8 +31,8 @@ namespace StardewModdingAPI.Framework.Logging /// <summary>Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary> private const char IgnoreChar = InterceptingTextWriter.IgnoreChar; - /// <summary>Get a named monitor instance.</summary> - private readonly Func<string, Monitor> GetMonitorImpl; + /// <summary>Create a monitor instance given the ID and name.</summary> + private readonly Func<string, string, Monitor> GetMonitorImpl; /// <summary>Regex patterns which match console non-error messages to suppress from the console and log.</summary> private readonly Regex[] SuppressConsolePatterns = @@ -88,23 +88,23 @@ namespace StardewModdingAPI.Framework.Logging /// <param name="logPath">The log file path to write.</param> /// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param> /// <param name="writeToConsole">Whether to output log messages to the console.</param> - /// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param> + /// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param> /// <param name="isDeveloperMode">Whether to enable full console output for developers.</param> /// <param name="getScreenIdForLog">Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</param> - public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode, Func<int?> getScreenIdForLog) + public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, HashSet<string> verboseLogging, bool isDeveloperMode, Func<int?> getScreenIdForLog) { // init log file this.LogFile = new LogFileManager(logPath); // init monitor - this.GetMonitorImpl = name => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog) + this.GetMonitorImpl = (id, name) => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, ShowFullStampInConsole = isDeveloperMode }; - this.Monitor = this.GetMonitor("SMAPI"); - this.MonitorForGame = this.GetMonitor("game"); + this.Monitor = this.GetMonitor("SMAPI", "SMAPI"); + this.MonitorForGame = this.GetMonitor("game", "game"); // redirect direct console output this.ConsoleInterceptor = new InterceptingTextWriter( @@ -124,10 +124,11 @@ namespace StardewModdingAPI.Framework.Logging } /// <summary>Get a monitor instance derived from SMAPI's current settings.</summary> + /// <param name="id">The unique ID for the mod context.</param> /// <param name="name">The name of the module which will log messages with this instance.</param> - public Monitor GetMonitor(string name) + public Monitor GetMonitor(string id, string name) { - return this.GetMonitorImpl(name); + return this.GetMonitorImpl(id, name); } /// <summary>Set the title of the SMAPI console window.</summary> diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 1c7cd3ed..baef6144 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -20,10 +20,9 @@ namespace StardewModdingAPI.Framework.Models [nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(), [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", - [nameof(VerboseLogging)] = false, [nameof(LogNetworkTraffic)] = false, [nameof(RewriteMods)] = true, - [nameof(UsePintail)] = true, + [nameof(UseRawImageLoading)] = true, [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux }; @@ -57,14 +56,15 @@ namespace StardewModdingAPI.Framework.Models /// <summary>The base URL for SMAPI's web API, used to perform update checks.</summary> public string WebApiBaseUrl { get; } - /// <summary>Whether SMAPI should log more information about the game context.</summary> - public bool VerboseLogging { get; } + /// <summary>The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</summary> + /// <remarks>The possible values are "*" (everything is verbose), "SMAPI", (SMAPI itself), or mod IDs.</remarks> + public HashSet<string> VerboseLogging { get; } /// <summary>Whether SMAPI should rewrite mods for compatibility.</summary> public bool RewriteMods { get; } - /// <summary>Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</summary> - public bool UsePintail { get; } + /// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary> + public bool UseRawImageLoading { get; } /// <summary>Whether to make SMAPI file APIs case-insensitive, even on Linux.</summary> public bool UseCaseInsensitivePaths { get; } @@ -76,7 +76,7 @@ namespace StardewModdingAPI.Framework.Models public ColorSchemeConfig ConsoleColors { get; } /// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary> - public string[] SuppressUpdateChecks { get; } + public HashSet<string> SuppressUpdateChecks { get; } /******** @@ -89,14 +89,14 @@ namespace StardewModdingAPI.Framework.Models /// <param name="useBetaChannel">Whether to show beta versions as valid updates.</param> /// <param name="gitHubProjectName">SMAPI's GitHub project name, used to perform update checks.</param> /// <param name="webApiBaseUrl">The base URL for SMAPI's web API, used to perform update checks.</param> - /// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param> + /// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param> /// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param> - /// <param name="usePintail">Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</param> + /// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param> /// <param name="useCaseInsensitivePaths">>Whether to make SMAPI file APIs case-insensitive, even on Linux.</param> /// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param> /// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param> /// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param> - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -104,13 +104,13 @@ namespace StardewModdingAPI.Framework.Models this.UseBetaChannel = useBetaChannel ?? (bool)SConfig.DefaultValues[nameof(this.UseBetaChannel)]; this.GitHubProjectName = gitHubProjectName; this.WebApiBaseUrl = webApiBaseUrl; - this.VerboseLogging = verboseLogging ?? (bool)SConfig.DefaultValues[nameof(this.VerboseLogging)]; + this.VerboseLogging = new HashSet<string>(verboseLogging ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase); this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)]; - this.UsePintail = usePintail ?? (bool)SConfig.DefaultValues[nameof(this.UsePintail)]; + this.UseRawImageLoading = useRawImageLoading ?? (bool)SConfig.DefaultValues[nameof(this.UseRawImageLoading)]; this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(this.UseCaseInsensitivePaths)]; this.LogNetworkTraffic = logNetworkTraffic ?? (bool)SConfig.DefaultValues[nameof(this.LogNetworkTraffic)]; this.ConsoleColors = consoleColors; - this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>(); + this.SuppressUpdateChecks = new HashSet<string>(suppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase); } /// <summary>Override the value of <see cref="DeveloperMode"/>.</summary> @@ -132,9 +132,11 @@ namespace StardewModdingAPI.Framework.Models custom[name] = value; } - HashSet<string> curSuppressUpdateChecks = new(this.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); - if (SConfig.DefaultSuppressUpdateChecks.Count != curSuppressUpdateChecks.Count || SConfig.DefaultSuppressUpdateChecks.Any(p => !curSuppressUpdateChecks.Contains(p))) - custom[nameof(this.SuppressUpdateChecks)] = "[" + string.Join(", ", this.SuppressUpdateChecks) + "]"; + if (!this.SuppressUpdateChecks.SetEquals(SConfig.DefaultSuppressUpdateChecks)) + custom[nameof(this.SuppressUpdateChecks)] = $"[{string.Join(", ", this.SuppressUpdateChecks)}]"; + + if (this.VerboseLogging.Any()) + custom[nameof(this.VerboseLogging)] = $"[{string.Join(", ", this.VerboseLogging)}]"; return custom; } diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs deleted file mode 100644 index 9576f768..00000000 --- a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// <summary>Generates a proxy class to access a mod API through an arbitrary interface.</summary> - internal class OriginalInterfaceProxyBuilder - { - /********* - ** Fields - *********/ - /// <summary>The target class type.</summary> - private readonly Type TargetType; - - /// <summary>The generated proxy type.</summary> - private readonly Type ProxyType; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="name">The type name to generate.</param> - /// <param name="moduleBuilder">The CLR module in which to create proxy classes.</param> - /// <param name="interfaceType">The interface type to implement.</param> - /// <param name="targetType">The target type.</param> - public OriginalInterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) - { - // validate - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (targetType == null) - throw new ArgumentNullException(nameof(targetType)); - - // define proxy type - TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); - proxyBuilder.AddInterfaceImplementation(interfaceType); - - // create field to store target instance - FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); - - // create constructor which accepts target instance and sets field - { - ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); - ILGenerator il = constructor.GetILGenerator(); - - il.Emit(OpCodes.Ldarg_0); // this - // ReSharper disable once AssignNullToNotNullAttribute -- never null - il.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)!); // call base constructor - il.Emit(OpCodes.Ldarg_0); // this - il.Emit(OpCodes.Ldarg_1); // load argument - il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument - il.Emit(OpCodes.Ret); - } - - // proxy methods - foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) - { - var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); - if (targetMethod == null) - throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); - - this.ProxyMethod(proxyBuilder, targetMethod, targetField); - } - - // save info - this.TargetType = targetType; - this.ProxyType = proxyBuilder.CreateType()!; - } - - /// <summary>Create an instance of the proxy for a target instance.</summary> - /// <param name="targetInstance">The target instance.</param> - public object CreateInstance(object targetInstance) - { - ConstructorInfo? constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); - if (constructor == null) - throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen - return constructor.Invoke(new[] { targetInstance }); - } - - - /********* - ** Private methods - *********/ - /// <summary>Define a method which proxies access to a method on the target.</summary> - /// <param name="proxyBuilder">The proxy type being generated.</param> - /// <param name="target">The target method.</param> - /// <param name="instanceField">The proxy field containing the API instance.</param> - private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) - { - Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray(); - - // create method - MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); - methodBuilder.SetParameters(argTypes); - methodBuilder.SetReturnType(target.ReturnType); - - // create method body - { - ILGenerator il = methodBuilder.GetILGenerator(); - - // load target instance - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, instanceField); - - // invoke target method on instance - for (int i = 0; i < argTypes.Length; i++) - il.Emit(OpCodes.Ldarg, i + 1); - il.Emit(OpCodes.Call, target); - - // return result - il.Emit(OpCodes.Ret); - } - } - } -} diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs deleted file mode 100644 index d6966978..00000000 --- a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// <inheritdoc /> - internal class OriginalInterfaceProxyFactory : IInterfaceProxyFactory - { - /********* - ** Fields - *********/ - /// <summary>The CLR module in which to create proxy classes.</summary> - private readonly ModuleBuilder ModuleBuilder; - - /// <summary>The generated proxy types.</summary> - private readonly IDictionary<string, OriginalInterfaceProxyBuilder> Builders = new Dictionary<string, OriginalInterfaceProxyBuilder>(); - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public OriginalInterfaceProxyFactory() - { - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); - this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); - } - - /// <inheritdoc /> - public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) - where TInterface : class - { - lock (this.Builders) - { - // validate - if (instance == null) - throw new InvalidOperationException("Can't proxy access to a null API."); - if (!typeof(TInterface).IsInterface) - throw new InvalidOperationException("The proxy type must be an interface, not a class."); - - // get proxy type - Type targetType = instance.GetType(); - string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; - if (!this.Builders.TryGetValue(proxyTypeName, out OriginalInterfaceProxyBuilder? builder)) - { - builder = new OriginalInterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType); - this.Builders[proxyTypeName] = builder; - } - - // create instance - return (TInterface)builder.CreateInstance(instance); - } - } - } -} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 731731d4..385a94ea 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -194,7 +194,7 @@ namespace StardewModdingAPI.Framework if (developerMode.HasValue) this.Settings.OverrideDeveloperMode(developerMode.Value); - this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog); + this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, verboseLogging: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog); this.CommandManager = new CommandManager(this.Monitor); this.EventManager = new EventManager(this.ModRegistry); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -1231,7 +1231,7 @@ namespace StardewModdingAPI.Framework } // make sure it's a content pack for the requesting mod - if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest.ContentPackFor?.UniqueID, mod.Manifest.UniqueID)) + if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest.ContentPackFor?.UniqueID, mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) { mod.LogAsModOnce($"{errorPrefix}: that isn't a content pack for this mod.", LogLevel.Warn); return null; @@ -1301,7 +1301,8 @@ namespace StardewModdingAPI.Framework onAssetLoaded: this.OnAssetLoaded, onAssetsInvalidated: this.OnAssetsInvalidated, getFileLookup: this.GetFileLookup, - requestAssetOperations: this.RequestAssetOperations + requestAssetOperations: this.RequestAssetOperations, + useRawImageLoading: this.Settings.UseRawImageLoading ); if (this.ContentCore.Language != this.Translator.LocaleEnum) this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); @@ -1507,7 +1508,7 @@ namespace StardewModdingAPI.Framework { try { - HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); + HashSet<string> suppressUpdateChecks = this.Settings.SuppressUpdateChecks; // prepare search model List<ModSearchEntryModel> searchMods = new List<ModSearchEntryModel>(); @@ -1608,10 +1609,8 @@ namespace StardewModdingAPI.Framework using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) { // init - HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); - IInterfaceProxyFactory proxyFactory = this.Settings.UsePintail - ? new InterfaceProxyFactory() - : new OriginalInterfaceProxyFactory(); + HashSet<string> suppressUpdateChecks = this.Settings.SuppressUpdateChecks; + IInterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); // load mods foreach (IModMetadata mod in mods) @@ -1637,6 +1636,14 @@ namespace StardewModdingAPI.Framework // initialize translations this.ReloadTranslations(loaded); + // set temporary PyTK compatibility mode + // This is part of a three-part fix for PyTK 1.23.0 and earlier. When removing this, + // search 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher. + { + IModInfo? pyTk = this.ModRegistry.Get("Platonymous.Toolkit"); + ModContentManager.EnablePyTkLegacyMode = pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.23.1"); + } + // initialize loaded non-content-pack mods this.Monitor.Log("Launching mods...", LogLevel.Debug); #pragma warning disable CS0612, CS0618 // deprecated code @@ -1827,7 +1834,7 @@ namespace StardewModdingAPI.Framework // load as content pack if (mod.IsContentPack) { - IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); + IMonitor monitor = this.LogManager.GetMonitor(manifest.UniqueID, mod.DisplayName); IFileLookup fileLookup = this.GetFileLookup(mod.DirectoryPath); GameContentHelper gameContentHelper = new(this.ContentCore, mod, mod.DisplayName, monitor, this.Reflection); IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), this.Reflection); @@ -1902,7 +1909,7 @@ namespace StardewModdingAPI.Framework } // init mod helpers - IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); + IMonitor monitor = this.LogManager.GetMonitor(manifest.UniqueID, mod.DisplayName); TranslationHelper translationHelper = new(mod, contentCore.GetLocale(), contentCore.Language); IModHelper modHelper; { @@ -1965,7 +1972,7 @@ namespace StardewModdingAPI.Framework ); // create mod helpers - IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name); + IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.UniqueID, packManifest.Name); GameContentHelper gameContentHelper = new(contentCore, fakeMod, packManifest.Name, packMonitor, this.Reflection); IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, fakeMod, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), this.Reflection); TranslationHelper packTranslationHelper = new(fakeMod, contentCore.GetLocale(), contentCore.Language); diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs index b5fc1f57..1fcda077 100644 --- a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs +++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs @@ -27,15 +27,6 @@ namespace MonoMod.Utils private static readonly object[] _NoArgs = Array.Empty<object>(); private static readonly object?[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null }; - private static readonly Type? t_RuntimeModule = - typeof(Module).Assembly - .GetType("System.Reflection.RuntimeModule"); - - private static readonly PropertyInfo? p_RuntimeModule_RuntimeType = - typeof(Module).Assembly - .GetType("System.Reflection.RuntimeModule") - ?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - private static readonly Type? t_RuntimeType = typeof(Type).Assembly .GetType("System.RuntimeType"); @@ -109,22 +100,6 @@ namespace MonoMod.Utils } } - public static Type? GetModuleType(this Module? module) - { - // Sadly we can't blindly resolve type 0x02000001 as the runtime throws ArgumentException. - - if (module == null || t_RuntimeModule == null || !t_RuntimeModule.IsInstanceOfType(module)) - return null; - - // .NET - if (p_RuntimeModule_RuntimeType != null) - return (Type?)p_RuntimeModule_RuntimeType.GetValue(module, _NoArgs); - - // The hotfix doesn't apply to Mono anyway, thus that's not copied over. - - return null; - } - public static Type? GetRealDeclaringType(this MemberInfo member) { return member.DeclaringType ?? member.Module.GetModuleType(); diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs index 6f8a4719..3e5b833d 100644 --- a/src/SMAPI/IAssetDataForImage.cs +++ b/src/SMAPI/IAssetDataForImage.cs @@ -18,6 +18,16 @@ namespace StardewModdingAPI /// <exception cref="ArgumentNullException">One of the arguments is null.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception> + void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); + + /// <summary>Overwrite part of the image.</summary> + /// <param name="source">The image to patch into the content.</param> + /// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> texture.</param> + /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param> + /// <param name="patchMode">Indicates how an image should be patched.</param> + /// <exception cref="ArgumentNullException">One of the arguments is null.</exception> + /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> + /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception> void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); /// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary> diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index 2cd0c1fc..7637edf0 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI ** Public methods *********/ /// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> - /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, dictionaries, and lists; other types may be supported by the game's content pipeline.</typeparam> + /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, <see cref="IRawTextureData"/> (for mod content only), and data structures; other types may be supported by the game's content pipeline.</typeparam> /// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param> /// <param name="source">Where to search for a matching content asset.</param> /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 1215fe0b..73b1a860 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -48,7 +48,7 @@ namespace StardewModdingAPI where TModel : class; /// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> - /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam> + /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, <see cref="IRawTextureData"/>, and data structures; other types may be supported by the game's content pipeline.</typeparam> /// <param name="key">The relative file path within the content pack (case-insensitive).</param> /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> diff --git a/src/SMAPI/IModContentHelper.cs b/src/SMAPI/IModContentHelper.cs index f1f6ce94..1e2d82a8 100644 --- a/src/SMAPI/IModContentHelper.cs +++ b/src/SMAPI/IModContentHelper.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI ** Public methods *********/ /// <summary>Load content from the mod folder and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> - /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, dictionaries, and lists; other types may be supported by the game's content pipeline.</typeparam> + /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, <see cref="IRawTextureData"/>, and data structures; other types may be supported by the game's content pipeline.</typeparam> /// <param name="relativePath">The local path to a content file relative to the mod folder.</param> /// <exception cref="ArgumentException">The <paramref name="relativePath"/> is empty or contains invalid characters.</exception> /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> diff --git a/src/SMAPI/IRawTextureData.cs b/src/SMAPI/IRawTextureData.cs new file mode 100644 index 00000000..a4da52f3 --- /dev/null +++ b/src/SMAPI/IRawTextureData.cs @@ -0,0 +1,17 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI +{ + /// <summary>The raw data for an image read from the filesystem.</summary> + public interface IRawTextureData + { + /// <summary>The image width.</summary> + int Width { get; } + + /// <summary>The image height.</summary> + int Height { get; } + + /// <summary>The loaded image data.</summary> + Color[] Data { get; } + } +} diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 8ed6b591..b783b2b9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -85,8 +85,8 @@ namespace StardewModdingAPI.Metadata /// <param name="assets">The asset keys and types to reload.</param> /// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param> /// <param name="propagatedAssets">A lookup of asset names to whether they've been propagated.</param> - /// <param name="updatedNpcWarps">Whether the NPC pathfinding cache was reloaded.</param> - public void Propagate(IDictionary<IAssetName, Type> assets, bool ignoreWorld, out IDictionary<IAssetName, bool> propagatedAssets, out bool updatedNpcWarps) + /// <param name="changedWarpRoutes">Whether the NPC pathfinding warp route cache was reloaded.</param> + public void Propagate(IDictionary<IAssetName, Type> assets, bool ignoreWorld, out IDictionary<IAssetName, bool> propagatedAssets, out bool changedWarpRoutes) { // get base name lookup propagatedAssets = assets @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Metadata }); // reload assets - updatedNpcWarps = false; + changedWarpRoutes = false; foreach (var bucket in buckets) { switch (bucket.Key) @@ -126,10 +126,10 @@ namespace StardewModdingAPI.Metadata foreach (var entry in bucket) { bool changed = false; - bool curChangedMapWarps = false; + bool curChangedMapRoutes = false; try { - changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapWarps); + changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapRoutes); } catch (Exception ex) { @@ -137,14 +137,14 @@ namespace StardewModdingAPI.Metadata } propagatedAssets[entry.Key] = changed; - updatedNpcWarps = updatedNpcWarps || curChangedMapWarps; + changedWarpRoutes = changedWarpRoutes || curChangedMapRoutes; } break; } } - // reload NPC pathfinding cache if any map changed - if (updatedNpcWarps) + // reload NPC pathfinding cache if any map routes changed + if (changedWarpRoutes) NPC.populateRoutesFromLocationToLocationList(); } @@ -156,14 +156,14 @@ namespace StardewModdingAPI.Metadata /// <param name="assetName">The asset name to reload.</param> /// <param name="type">The asset type to reload.</param> /// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param> - /// <param name="changedWarps">Whether any map warps were changed as part of this propagation.</param> + /// <param name="changedWarpRoutes">Whether the locations reachable by warps from this location changed as part of this propagation.</param> /// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns> [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarps) + private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarpRoutes) { var content = this.MainContentManager; string key = assetName.BaseName; - changedWarps = false; + changedWarpRoutes = false; /**** ** Special case: current map tilesheet @@ -197,7 +197,7 @@ namespace StardewModdingAPI.Metadata static ISet<string> GetWarpSet(GameLocation location) { return new HashSet<string>( - location.warps.Select(p => $"{p.X} {p.Y} {p.TargetName} {p.TargetX} {p.TargetY}") + location.warps.Select(p => p.TargetName) ); } @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Metadata this.UpdateMap(info); var newWarps = GetWarpSet(location); - changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p)); + changedWarpRoutes = changedWarpRoutes || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p)); anyChanged = true; } } diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs index daa58943..afff16b8 100644 --- a/src/SMAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -3,3 +3,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("SMAPI.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing [assembly: InternalsVisibleTo("ContentPatcher")] +[assembly: InternalsVisibleTo("ErrorHandler")] diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 8324f45b..97e8e00c 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -16,9 +16,17 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ { /** - * Whether SMAPI should log more information about the game context. + * The logs for which to enable verbose logging, which may show a lot more information to + * simplify troubleshooting. + * + * The possible values are: + * - "*" for everything (not recommended); + * - "SMAPI" for messages from SMAPI itself; + * - mod IDs from their manifest.json files. + * + * For example: [ "SMAPI", "Pathoschild.ContentPatcher" ] */ - "VerboseLogging": false, + "VerboseLogging": [], /** * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new @@ -53,6 +61,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future "UsePintail": true, /** + * Whether to use raw image data when possible, instead of initializing an XNA Texture2D + * instance through the GPU. + */ + "UseRawImageLoading": true, + + /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as * part of their normal functionality, so these warnings are meaningless without further diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index a0ca54cc..3abefeab 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -23,9 +23,9 @@ <ItemGroup> <PackageReference Include="LargeAddressAware" Version="1.0.5" /> <PackageReference Include="Mono.Cecil" Version="0.11.4" /> - <PackageReference Include="MonoMod.Common" Version="21.6.21.1" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="Pintail" Version="2.1.0" /> + <PackageReference Include="MonoMod.Common" Version="22.3.5.1" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Pintail" Version="2.2.0" /> <PackageReference Include="Platonymous.TMXTile" Version="1.5.9" /> <PackageReference Include="System.Reflection.Emit" Version="4.7.0" /> @@ -41,6 +41,7 @@ <Reference Include="GalaxyCSharp" HintPath="$(GamePath)\GalaxyCSharp.dll" Private="False" /> <Reference Include="Lidgren.Network" HintPath="$(GamePath)\Lidgren.Network.dll" Private="False" /> <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" /> + <Reference Include="SkiaSharp" HintPath="$(GamePath)\SkiaSharp.dll" Private="False" /> <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" /> </ItemGroup> |