Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2022-06-16 22:14:44 -04:00
commit 8e9237bdd7
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
40 changed files with 1122 additions and 426 deletions

Binary file not shown.

View File

@ -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>
@ -475,6 +478,13 @@
<param name="argumentTypes">An array of argument types to target overloads</param>
<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>
@ -796,6 +806,13 @@
<param name="expression">The lambda expression using the method</param>
<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>
@ -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)">
@ -1760,6 +1782,12 @@
<param name="name">The name of the field</param>
<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>
@ -1767,6 +1795,12 @@
<param name="name">The name of the field (case sensitive)</param>
<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>
@ -1781,6 +1815,12 @@
<param name="name">The name of the property (case sensitive)</param>
<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>
@ -1788,6 +1828,12 @@
<param name="name">The name of the property (case sensitive)</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.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>
@ -1795,6 +1841,12 @@
<param name="name">The name of the property (case sensitive)</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.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>
@ -1802,6 +1854,12 @@
<param name="name">The name</param>
<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>
@ -1809,6 +1867,12 @@
<param name="name">The name</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.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>
@ -1816,6 +1880,12 @@
<param name="name">The name</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.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>
@ -1825,6 +1895,14 @@
<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>
</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>
@ -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">
@ -2281,6 +2370,13 @@
<param name="fieldName">The name of the field</param>
<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>
@ -2336,6 +2432,34 @@
</para>
</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>
@ -2508,6 +2632,412 @@
<param name="objects">The objects</param>
<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">
@ -2859,6 +3394,11 @@
<summary>Log a string directly to disk. 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.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>

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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());
}
}
}
}

View File

@ -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>

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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() };
/*********

View File

@ -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>

View File

@ -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'" />

View File

@ -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" />

View File

@ -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",

View File

@ -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

View File

@ -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>

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
@ -28,74 +29,62 @@ namespace StardewModdingAPI.Framework.Content
public AssetDataForImage(string? locale, IAssetName assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
/// <inheritdoc />
public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
{
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 null source data.");
// 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;
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)
{
// get texture
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.");
Texture2D target = this.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));
// validate
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
if (patchMode == PatchMode.Overlay)
{
// get target data
Color[] targetData = GC.AllocateUninitializedArray<Color>(pixelCount);
target.GetData(0, targetArea, targetData, 0, pixelCount);
// merge pixels
for (int i = 0; i < sourceData.Length; i++)
{
Color above = sourceData[i];
Color below = targetData[i];
// shortcut transparency
if (above.A < MinOpacity)
{
sourceData[i] = below;
continue;
}
if (below.A < MinOpacity)
{
sourceData[i] = above;
continue;
}
// 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
);
}
}
// patch target texture
target.SetData(0, targetArea, sourceData, 0, pixelCount);
// apply
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
}
/// <inheritdoc />
@ -110,5 +99,85 @@ namespace StardewModdingAPI.Framework.Content
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[] mergedData = GC.AllocateUninitializedArray<Color>(pixelCount);
target.GetData(0, targetArea, mergedData, 0, pixelCount);
// merge pixels
for (int i = 0; i < pixelCount; i++)
{
Color above = sourceData[i];
Color below = mergedData[i];
// shortcut transparency
if (above.A < MinOpacity)
continue;
if (below.A < MinOpacity)
mergedData[i] = above;
// merge pixels
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)
);
}
}
target.SetData(0, targetArea, mergedData, 0, pixelCount);
}
else
target.SetData(0, targetArea, sourceData, 0, pixelCount);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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());
}

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -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();

View File

@ -10,6 +10,16 @@ namespace StardewModdingAPI
/*********
** Public methods
*********/
/// <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(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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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")]

View File

@ -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
@ -52,6 +60,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

View File

@ -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>