summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs15
-rw-r--r--src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs26
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs1
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs1
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs5
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs1
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs35
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs65
-rw-r--r--src/SMAPI/Framework/Patching/GamePatcher.cs53
-rw-r--r--src/SMAPI/Framework/Patching/IHarmonyPatch.cs23
-rw-r--r--src/SMAPI/Framework/Patching/PatchHelper.cs36
-rw-r--r--src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs33
-rw-r--r--src/SMAPI/Framework/SCore.cs19
-rw-r--r--src/SMAPI/Framework/SGame.cs1
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs239
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs17
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs8
-rw-r--r--src/SMAPI/Patches/Game1Patcher.cs (renamed from src/SMAPI/Patches/LoadContextPatch.cs)83
-rw-r--r--src/SMAPI/Patches/TitleMenuPatcher.cs55
-rw-r--r--src/SMAPI/Program.cs32
-rw-r--r--src/SMAPI/Properties/AssemblyInfo.cs1
-rw-r--r--src/SMAPI/SMAPI.csproj4
26 files changed, 468 insertions, 299 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 9e93551c..3877e17a 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -61,7 +61,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.11.0";
+ internal static string RawApiVersion = "3.12.0";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -351,9 +351,16 @@ namespace StardewModdingAPI
DirectoryInfo folder = null;
foreach (string saveName in new[] { rawSaveName, new string(rawSaveName.Where(char.IsLetterOrDigit).ToArray()) })
{
- folder = new DirectoryInfo(Path.Combine(Constants.SavesPath, $"{saveName}_{saveID}"));
- if (folder.Exists)
- return folder;
+ try
+ {
+ folder = new DirectoryInfo(Path.Combine(Constants.SavesPath, $"{saveName}_{saveID}"));
+ if (folder.Exists)
+ return folder;
+ }
+ catch (ArgumentException)
+ {
+ // ignore invalid path
+ }
}
// if save doesn't exist yet, return the default one we expect to be created
diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
index f3731d16..45b34556 100644
--- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
+++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
@@ -3,25 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
-#if HARMONY_2
using HarmonyLib;
-#else
-using Harmony;
-#endif
namespace StardewModdingAPI.Framework.Commands
{
/// <summary>The 'harmony_summary' SMAPI console command.</summary>
internal class HarmonySummaryCommand : IInternalCommand
{
-#if !HARMONY_2
- /*********
- ** Fields
- *********/
- /// <summary>The Harmony instance through which to fetch patch info.</summary>
- private readonly HarmonyInstance HarmonyInstance = HarmonyInstance.Create($"SMAPI.{nameof(HarmonySummaryCommand)}");
-#endif
-
/*********
** Accessors
*********/
@@ -60,9 +48,7 @@ namespace StardewModdingAPI.Framework.Commands
{
PatchType.Prefix => 0,
PatchType.Postfix => 1,
-#if HARMONY_2
PatchType.Finalizer => 2,
-#endif
PatchType.Transpiler => 3,
_ => 4
});
@@ -111,26 +97,16 @@ namespace StardewModdingAPI.Framework.Commands
/// <summary>Get all current Harmony patches.</summary>
private IEnumerable<SearchResult> GetAllPatches()
{
-#if HARMONY_2
foreach (MethodBase method in Harmony.GetAllPatchedMethods().ToArray())
-#else
- foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods().ToArray())
-#endif
{
// get metadata for method
-#if HARMONY_2
HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method);
-#else
- Harmony.Patches patchInfo = this.HarmonyInstance.GetPatchInfo(method);
-#endif
IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>>
{
[PatchType.Prefix] = patchInfo.Prefixes,
[PatchType.Postfix] = patchInfo.Postfixes,
-#if HARMONY_2
[PatchType.Finalizer] = patchInfo.Finalizers,
-#endif
[PatchType.Transpiler] = patchInfo.Transpilers
};
@@ -160,10 +136,8 @@ namespace StardewModdingAPI.Framework.Commands
/// <summary>A postfix patch.</summary>
Postfix,
-#if HARMONY_2
/// <summary>A finalizer patch.</summary>
Finalizer,
-#endif
/// <summary>A transpiler patch.</summary>
Transpiler
diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
index 037d9f89..10488b84 100644
--- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
+++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
@@ -1,5 +1,6 @@
using System;
using System.Reflection;
+using StardewModdingAPI.Internal;
namespace StardewModdingAPI.Framework.Content
{
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 80a9937a..63cd1759 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -9,6 +9,7 @@ using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Internal;
using StardewValley;
using xTile;
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 4f6aa775..d24ffb81 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@@ -77,6 +78,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <inheritdoc />
public override T Load<T>(string assetName, LanguageCode language, bool useCache)
{
+ // normalize key
+ bool isXnbFile = Path.GetExtension(assetName).ToLower() == ".xnb";
assetName = this.AssertAndNormalizeAssetName(assetName);
// disable caching
@@ -108,7 +111,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
try
{
// get file
- FileInfo file = this.GetModFile(assetName);
+ FileInfo file = this.GetModFile(isXnbFile ? $"{assetName}.xnb" : assetName); // .xnb extension is stripped from asset names passed to the content manager
if (!file.Exists)
throw GetContentError("the specified path doesn't exist.");
diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs
index 2204966c..fa20a079 100644
--- a/src/SMAPI/Framework/Events/ManagedEvent.cs
+++ b/src/SMAPI/Framework/Events/ManagedEvent.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using StardewModdingAPI.Events;
+using StardewModdingAPI.Internal;
namespace StardewModdingAPI.Framework.Events
{
diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs
index ab7f1e6c..6c9a5f3b 100644
--- a/src/SMAPI/Framework/InternalExtensions.cs
+++ b/src/SMAPI/Framework/InternalExtensions.cs
@@ -14,6 +14,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Provides extension methods for SMAPI's internal use.</summary>
internal static class InternalExtensions
{
+ /*********
+ ** Public methods
+ *********/
/****
** IMonitor
****/
@@ -55,38 +58,6 @@ namespace StardewModdingAPI.Framework
}
/****
- ** Exceptions
- ****/
- /// <summary>Get a string representation of an exception suitable for writing to the error log.</summary>
- /// <param name="exception">The error to summarize.</param>
- public static string GetLogSummary(this Exception exception)
- {
- switch (exception)
- {
- case TypeLoadException ex:
- return $"Failed loading type '{ex.TypeName}': {exception}";
-
- case ReflectionTypeLoadException ex:
- string summary = exception.ToString();
- foreach (Exception childEx in ex.LoaderExceptions)
- summary += $"\n\n{childEx.GetLogSummary()}";
- return summary;
-
- default:
- return exception.ToString();
- }
- }
-
- /// <summary>Get the lowest exception in an exception stack.</summary>
- /// <param name="exception">The exception from which to search.</param>
- public static Exception GetInnermostException(this Exception exception)
- {
- while (exception.InnerException != null)
- exception = exception.InnerException;
- return exception;
- }
-
- /****
** ReaderWriterLockSlim
****/
/// <summary>Run code within a read lock.</summary>
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index a4df3c18..a3d4f23d 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using StardewModdingAPI.Framework.Commands;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.ConsoleWriting;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Utilities;
@@ -106,6 +108,10 @@ namespace StardewModdingAPI.Framework.Logging
if (writeToConsole)
output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
Console.SetOut(output);
+
+ // enable Unicode handling
+ Console.InputEncoding = Encoding.Unicode;
+ Console.OutputEncoding = Encoding.Unicode;
}
/// <summary>Get a monitor instance derived from SMAPI's current settings.</summary>
@@ -258,7 +264,7 @@ namespace StardewModdingAPI.Framework.Logging
break;
// path too long exception
- case PathTooLongException:
+ case PathTooLongException _:
{
string[] affectedPaths = PathUtilities.GetTooLongPaths(Constants.ModsPath).ToArray();
string message = affectedPaths.Any()
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs
index 102f3364..8e4320b3 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs
@@ -1,4 +1,3 @@
-#if HARMONY_2
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -41,4 +40,3 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
}
}
}
-#endif
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
index ad6d5e4f..54b91679 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
@@ -1,4 +1,3 @@
-#if HARMONY_2
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -81,4 +80,3 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
}
}
}
-#endif
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs
index f3975558..44c97401 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs
@@ -1,4 +1,3 @@
-#if HARMONY_2
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -44,4 +43,3 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
}
}
}
-#endif
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs
index 4b3675bc..7a3b428d 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs
@@ -1,4 +1,3 @@
-#if HARMONY_2
using System;
using HarmonyLib;
using Mono.Cecil;
@@ -29,11 +28,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith)
{
// rewrite Harmony 1.x type to Harmony 2.0 type
- if (type.Scope is AssemblyNameReference scope && scope.Name == "0Harmony" && scope.Version.Major == 1)
+ if (type.Scope is AssemblyNameReference { Name: "0Harmony" } scope && scope.Version.Major == 1)
{
Type targetType = this.GetMappedType(type);
replaceWith(module.ImportReference(targetType));
- this.MarkRewritten();
+ this.OnChanged();
this.ReplacedTypes = true;
return true;
}
@@ -42,19 +41,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
}
/// <inheritdoc />
- public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
+ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// rewrite Harmony 1.x methods to Harmony 2.0
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
if (this.TryRewriteMethodsToFacade(module, methodRef))
+ {
+ this.OnChanged();
return true;
+ }
// rewrite renamed fields
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null)
{
if (fieldRef.DeclaringType.FullName == "HarmonyLib.HarmonyMethod" && fieldRef.Name == "prioritiy")
+ {
fieldRef.Name = nameof(HarmonyMethod.priority);
+ this.OnChanged();
+ }
}
return false;
@@ -64,6 +69,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/*********
** Private methods
*********/
+ /// <summary>Update the mod metadata when any Harmony 1.x code is migrated.</summary>
+ private void OnChanged()
+ {
+ this.MarkRewritten();
+ this.MarkFlag(InstructionHandleResult.DetectedGamePatch);
+ }
+
/// <summary>Rewrite methods to use Harmony facades if needed.</summary>
/// <param name="module">The assembly module containing the method reference.</param>
/// <param name="methodRef">The method reference to map.</param>
@@ -73,24 +85,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
return false; // not Harmony (or already using Harmony 2.0)
// get facade type
- Type toType;
- switch (methodRef?.DeclaringType.FullName)
+ Type toType = methodRef?.DeclaringType.FullName switch
{
- case "HarmonyLib.Harmony":
- toType = typeof(HarmonyInstanceFacade);
- break;
-
- case "HarmonyLib.AccessTools":
- toType = typeof(AccessToolsFacade);
- break;
-
- case "HarmonyLib.HarmonyMethod":
- toType = typeof(HarmonyMethodFacade);
- break;
-
- default:
- return false;
- }
+ "HarmonyLib.Harmony" => typeof(HarmonyInstanceFacade),
+ "HarmonyLib.AccessTools" => typeof(AccessToolsFacade),
+ "HarmonyLib.HarmonyMethod" => typeof(HarmonyMethodFacade),
+ _ => null
+ };
+ if (toType == null)
+ return false;
// map if there's a matching method
if (RewriteHelper.HasMatchingSignature(toType, methodRef))
@@ -103,18 +106,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
}
/// <summary>Get an equivalent Harmony 2.x type.</summary>
- /// <param name="type">The Harmony 1.x method.</param>
+ /// <param name="type">The Harmony 1.x type.</param>
private Type GetMappedType(TypeReference type)
{
- // main Harmony object
- if (type.FullName == "Harmony.HarmonyInstance")
- return typeof(Harmony);
+ return type.FullName switch
+ {
+ "Harmony.HarmonyInstance" => typeof(Harmony),
+ "Harmony.ILCopying.ExceptionBlock" => typeof(ExceptionBlock),
+ _ => this.GetMappedTypeByConvention(type)
+ };
+ }
- // other objects
+ /// <summary>Get an equivalent Harmony 2.x type using the convention expected for most types.</summary>
+ /// <param name="type">The Harmony 1.x type.</param>
+ private Type GetMappedTypeByConvention(TypeReference type)
+ {
string fullName = type.FullName.Replace("Harmony.", "HarmonyLib.");
- string targetName = typeof(Harmony).AssemblyQualifiedName.Replace(typeof(Harmony).FullName, fullName);
+ string targetName = typeof(Harmony).AssemblyQualifiedName!.Replace(typeof(Harmony).FullName!, fullName);
return Type.GetType(targetName, throwOnError: true);
}
}
}
-#endif
diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs
deleted file mode 100644
index ddecda08..00000000
--- a/src/SMAPI/Framework/Patching/GamePatcher.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-#if HARMONY_2
-using HarmonyLib;
-#else
-using Harmony;
-#endif
-
-namespace StardewModdingAPI.Framework.Patching
-{
- /// <summary>Encapsulates applying Harmony patches to the game.</summary>
- internal class GamePatcher
- {
- /*********
- ** Fields
- *********/
- /// <summary>Encapsulates monitoring and logging.</summary>
- private readonly IMonitor Monitor;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- public GamePatcher(IMonitor monitor)
- {
- this.Monitor = monitor;
- }
-
- /// <summary>Apply all loaded patches to the game.</summary>
- /// <param name="patches">The patches to apply.</param>
- public void Apply(params IHarmonyPatch[] patches)
- {
-#if HARMONY_2
- Harmony harmony = new Harmony("SMAPI");
-#else
- HarmonyInstance harmony = HarmonyInstance.Create("SMAPI");
-#endif
- foreach (IHarmonyPatch patch in patches)
- {
- try
- {
- patch.Apply(harmony);
- }
- catch (Exception ex)
- {
- this.Monitor.Log($"Couldn't apply runtime patch '{patch.GetType().Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error);
- this.Monitor.Log(ex.GetLogSummary(), LogLevel.Trace);
- }
- }
- }
- }
-}
diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs
deleted file mode 100644
index 38d30ab2..00000000
--- a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#if HARMONY_2
-using HarmonyLib;
-#else
-using Harmony;
-#endif
-
-namespace StardewModdingAPI.Framework.Patching
-{
- /// <summary>A Harmony patch to apply.</summary>
- internal interface IHarmonyPatch
- {
- /*********
- ** Methods
- *********/
- /// <summary>Apply the Harmony patch.</summary>
- /// <param name="harmony">The Harmony instance.</param>
-#if HARMONY_2
- void Apply(Harmony harmony);
-#else
- void Apply(HarmonyInstance harmony);
-#endif
- }
-}
diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs
deleted file mode 100644
index d1aa0185..00000000
--- a/src/SMAPI/Framework/Patching/PatchHelper.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-#if !HARMONY_2
-using System;
-using System.Collections.Generic;
-
-namespace StardewModdingAPI.Framework.Patching
-{
- /// <summary>Provides generic methods for implementing Harmony patches.</summary>
- internal class PatchHelper
- {
- /*********
- ** Fields
- *********/
- /// <summary>The interception keys currently being intercepted.</summary>
- private static readonly HashSet<string> InterceptingKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Track a method that will be intercepted.</summary>
- /// <param name="key">The intercept key.</param>
- /// <returns>Returns false if the method was already marked for interception, else true.</returns>
- public static bool StartIntercept(string key)
- {
- return PatchHelper.InterceptingKeys.Add(key);
- }
-
- /// <summary>Track a method as no longer being intercepted.</summary>
- /// <param name="key">The intercept key.</param>
- public static void StopIntercept(string key)
- {
- PatchHelper.InterceptingKeys.Remove(key);
- }
- }
-}
-#endif
diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
index 464367b6..8d1b6034 100644
--- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
+++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
@@ -36,23 +36,26 @@ namespace StardewModdingAPI.Framework.Reflection
public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
where TInterface : class
{
- // 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 InterfaceProxyBuilder builder))
+ lock (this.Builders)
{
- builder = new InterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType);
- this.Builders[proxyTypeName] = builder;
- }
+ // 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.");
- // create instance
- return (TInterface)builder.CreateInstance(instance);
+ // 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 InterfaceProxyBuilder builder))
+ {
+ builder = new InterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType);
+ this.Builders[proxyTypeName] = builder;
+ }
+
+ // create instance
+ return (TInterface)builder.CreateInstance(instance);
+ }
}
}
}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index c3285979..a34b3eff 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
@@ -30,13 +31,14 @@ using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModHelpers;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Framework.Networking;
-using StardewModdingAPI.Framework.Patching;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Rendering;
using StardewModdingAPI.Framework.Serialization;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Framework.StateTracking.Snapshots;
using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Internal;
+using StardewModdingAPI.Internal.Patching;
using StardewModdingAPI.Patches;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
@@ -46,6 +48,7 @@ using StardewModdingAPI.Toolkit.Utilities;
using StardewModdingAPI.Utilities;
using StardewValley;
using xTile.Display;
+using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
using SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework
@@ -251,8 +254,10 @@ namespace StardewModdingAPI.Framework
StardewValley.GameRunner.instance = this.Game;
// apply game patches
- new GamePatcher(this.Monitor).Apply(
- new LoadContextPatch(this.Reflection, this.OnLoadStageChanged)
+ MiniMonoModHotfix.Apply();
+ HarmonyPatcher.Apply("SMAPI", this.Monitor,
+ new Game1Patcher(this.Reflection, this.OnLoadStageChanged),
+ new TitleMenuPatcher(this.OnLoadStageChanged)
);
// add exit handler
@@ -308,6 +313,14 @@ namespace StardewModdingAPI.Framework
}
}
+ /// <summary>Get the core logger and monitor on behalf of the game.</summary>
+ /// <remarks>This method is called using reflection by the ErrorHandler mod to log game errors.</remarks>
+ [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via reflection")]
+ public IMonitor GetMonitorForGame()
+ {
+ return this.LogManager.MonitorForGame;
+ }
+
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index af7fa387..55ab8377 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.StateTracking.Snapshots;
using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.BellsAndWhistles;
diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
new file mode 100644
index 00000000..9d63ab2c
--- /dev/null
+++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
@@ -0,0 +1,239 @@
+// This temporary utility fixes an esoteric issue in XNA Framework where deserialization depends on
+// the order of fields returned by Type.GetFields, but that order changes after Harmony/MonoMod use
+// reflection to access the fields due to an issue in .NET Framework.
+// https://twitter.com/0x0ade/status/1414992316964687873
+//
+// This will be removed when Harmony/MonoMod are updated to incorporate the fix.
+//
+// Special thanks to 0x0ade for submitting this worokaround! Copy/pasted and adapted from MonoMod.
+
+using System;
+using System.Reflection;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using HarmonyLib;
+using System.Reflection.Emit;
+
+// ReSharper disable once CheckNamespace -- Temporary hotfix submitted by the MonoMod author.
+namespace MonoMod.Utils
+{
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Temporary hotfix submitted by the MonoMod author.")]
+ [SuppressMessage("ReSharper", "PossibleNullReferenceException", Justification = "Temporary hotfix submitted by the MonoMod author.")]
+ static class MiniMonoModHotfix
+ {
+ // .NET Framework can break member ordering if using Module.Resolve* on certain members.
+
+ private static object[] _NoArgs = new object[0];
+ private static object[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null };
+
+ private static Type t_RuntimeModule =
+ typeof(Module).Assembly
+ .GetType("System.Reflection.RuntimeModule");
+
+ private static PropertyInfo p_RuntimeModule_RuntimeType =
+ typeof(Module).Assembly
+ .GetType("System.Reflection.RuntimeModule")
+ ?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+
+ private static Type t_RuntimeType =
+ typeof(Type).Assembly
+ .GetType("System.RuntimeType");
+
+ private static PropertyInfo p_RuntimeType_Cache =
+ typeof(Type).Assembly
+ .GetType("System.RuntimeType")
+ ?.GetProperty("Cache", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+
+ private static MethodInfo m_RuntimeTypeCache_GetFieldList =
+ typeof(Type).Assembly
+ .GetType("System.RuntimeType+RuntimeTypeCache")
+ ?.GetMethod("GetFieldList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+
+ private static MethodInfo m_RuntimeTypeCache_GetPropertyList =
+ typeof(Type).Assembly
+ .GetType("System.RuntimeType+RuntimeTypeCache")
+ ?.GetMethod("GetPropertyList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+
+ private static readonly ConditionalWeakTable<Type, CacheFixEntry> _CacheFixed = new ConditionalWeakTable<Type, CacheFixEntry>();
+
+ public static void Apply()
+ {
+ var harmony = new Harmony("MiniMonoModHotfix");
+
+ harmony.Patch(
+ original: typeof(Harmony).Assembly
+ .GetType("HarmonyLib.MethodBodyReader")
+ .GetMethod("ReadOperand", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance),
+ transpiler: new HarmonyMethod(typeof(MiniMonoModHotfix), nameof(ResolveTokenFix))
+ );
+
+ harmony.Patch(
+ original: typeof(MonoMod.Utils.ReflectionHelper).Assembly
+ .GetType("MonoMod.Utils.DynamicMethodDefinition+<>c__DisplayClass3_0")
+ .GetMethod("<_CopyMethodToDefinition>g__ResolveTokenAs|1", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance),
+ transpiler: new HarmonyMethod(typeof(MiniMonoModHotfix), nameof(ResolveTokenFix))
+ );
+
+ }
+
+ private static IEnumerable<CodeInstruction> ResolveTokenFix(IEnumerable<CodeInstruction> instrs)
+ {
+ MethodInfo getdecl = typeof(MiniMonoModHotfix).GetMethod(nameof(GetRealDeclaringType));
+ MethodInfo fixup = typeof(MiniMonoModHotfix).GetMethod(nameof(FixReflectionCache));
+
+ foreach (CodeInstruction instr in instrs)
+ {
+ yield return instr;
+
+ if (instr.operand is MethodInfo called)
+ {
+ switch (called.Name)
+ {
+ case "ResolveType":
+ // type.FixReflectionCache();
+ yield return new CodeInstruction(OpCodes.Dup);
+ yield return new CodeInstruction(OpCodes.Call, fixup);
+ break;
+
+ case "ResolveMember":
+ case "ResolveMethod":
+ case "ResolveField":
+ // member.GetRealDeclaringType().FixReflectionCache();
+ yield return new CodeInstruction(OpCodes.Dup);
+ yield return new CodeInstruction(OpCodes.Call, getdecl);
+ yield return new CodeInstruction(OpCodes.Call, fixup);
+ break;
+ }
+ }
+ }
+ }
+
+ 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)
+ => member.DeclaringType ?? member.Module?.GetModuleType();
+
+ public static void FixReflectionCache(this Type type)
+ {
+ if (t_RuntimeType == null ||
+ p_RuntimeType_Cache == null ||
+ m_RuntimeTypeCache_GetFieldList == null ||
+ m_RuntimeTypeCache_GetPropertyList == null)
+ return;
+
+ for (; type != null; type = type.DeclaringType)
+ {
+ // All types SHOULD inherit RuntimeType, including those built at runtime.
+ // One might never know what awaits us in the depths of reflection hell though.
+ if (!t_RuntimeType.IsInstanceOfType(type))
+ continue;
+
+ CacheFixEntry entry = _CacheFixed.GetValue(type, rt => {
+ CacheFixEntry entryNew = new CacheFixEntry();
+ object cache;
+ Array properties, fields;
+
+ // All RuntimeTypes MUST have a cache, the getter is non-virtual, it creates on demand and asserts non-null.
+ entryNew.Cache = cache = p_RuntimeType_Cache.GetValue(rt, _NoArgs);
+ entryNew.Properties = properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList);
+ entryNew.Fields = fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList);
+
+ _FixReflectionCacheOrder<PropertyInfo>(properties);
+ _FixReflectionCacheOrder<FieldInfo>(fields);
+
+ entryNew.NeedsVerify = false;
+ return entryNew;
+ });
+
+ if (entry.NeedsVerify && !_Verify(entry, type))
+ {
+ lock (entry)
+ {
+ _FixReflectionCacheOrder<PropertyInfo>(entry.Properties);
+ _FixReflectionCacheOrder<FieldInfo>(entry.Fields);
+ }
+ }
+
+ entry.NeedsVerify = true;
+ }
+ }
+
+ private static bool _Verify(CacheFixEntry entry, Type type)
+ {
+ object cache;
+ Array properties, fields;
+
+ // The cache can sometimes be invalidated.
+ // TODO: Figure out if only the arrays get replaced or if the entire cache object gets replaced!
+ if (entry.Cache != (cache = p_RuntimeType_Cache.GetValue(type, _NoArgs)))
+ {
+ entry.Cache = cache;
+ entry.Properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList);
+ entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList);
+ return false;
+
+ }
+ else if (entry.Properties != (properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList)))
+ {
+ entry.Properties = properties;
+ entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList);
+ return false;
+
+ }
+ else if (entry.Fields != (fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList)))
+ {
+ entry.Fields = fields;
+ return false;
+
+ }
+ else
+ {
+ // Cache should still be the same, no re-fix necessary.
+ return true;
+ }
+ }
+
+ private static Array _GetArray(object cache, MethodInfo getter)
+ {
+ // Get and discard once, otherwise we might not be getting the actual backing array.
+ getter.Invoke(cache, _CacheGetterArgs);
+ return (Array)getter.Invoke(cache, _CacheGetterArgs);
+ }
+
+ private static void _FixReflectionCacheOrder<T>(Array orig) where T : MemberInfo
+ {
+ // Sort using a short-lived list.
+ List<T> list = new List<T>(orig.Length);
+ for (int i = 0; i < orig.Length; i++)
+ list.Add((T)orig.GetValue(i));
+
+ list.Sort((a, b) => a.MetadataToken - b.MetadataToken);
+
+ for (int i = orig.Length - 1; i >= 0; --i)
+ orig.SetValue(list[i], i);
+ }
+
+ private class CacheFixEntry
+ {
+ public object Cache;
+ public Array Properties;
+ public Array Fields;
+ public bool NeedsVerify;
+ }
+ }
+}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 5641f90f..a8686ca4 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -5,9 +5,9 @@ using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Graphics;
using Netcode;
-using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using StardewValley.BellsAndWhistles;
@@ -938,16 +938,17 @@ namespace StardewModdingAPI.Metadata
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
location.reloadMap();
- location.updateWarps();
- location.MakeMapModifications(force: true);
- // update interior doors
+ // reload interior doors
location.interiorDoors.Clear();
- foreach (var entry in new InteriorDoorDictionary(location))
- location.interiorDoors.Add(entry);
+ location.interiorDoors.ResetSharedState(); // load doors from map properties
+ location.interiorDoors.ResetLocalState(); // reapply door tiles
- // update doors
- location.doors.Clear();
+ // reapply map changes (after reloading doors so they apply theirs too)
+ location.MakeMapModifications(force: true);
+
+ // update for changes
+ location.updateWarps();
location.updateDoors();
}
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index d1699636..a787993a 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -48,10 +48,8 @@ namespace StardewModdingAPI.Metadata
yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies);
yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies);
-#if HARMONY_2
- // rewrite for SMAPI 3.x (Harmony 1.x => 2.0 update)
+ // rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update)
yield return new Harmony1AssemblyRewriter();
-#endif
}
/****
@@ -64,11 +62,7 @@ namespace StardewModdingAPI.Metadata
/****
** detect code which may impact game stability
****/
-#if HARMONY_2
yield return new TypeFinder(typeof(HarmonyLib.Harmony).FullName, InstructionHandleResult.DetectedGamePatch);
-#else
- yield return new TypeFinder(typeof(Harmony.HarmonyInstance).FullName, InstructionHandleResult.DetectedGamePatch);
-#endif
yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic);
yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerializer);
yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerializer);
diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/Game1Patcher.cs
index c43d7071..173a2055 100644
--- a/src/SMAPI/Patches/LoadContextPatch.cs
+++ b/src/SMAPI/Patches/Game1Patcher.cs
@@ -1,24 +1,20 @@
using System;
using System.Diagnostics.CodeAnalysis;
-#if HARMONY_2
using HarmonyLib;
-#else
-using Harmony;
-#endif
using StardewModdingAPI.Enums;
-using StardewModdingAPI.Framework.Patching;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Internal.Patching;
using StardewValley;
using StardewValley.Menus;
using StardewValley.Minigames;
namespace StardewModdingAPI.Patches
{
- /// <summary>Harmony patches which notify SMAPI for save creation load stages.</summary>
+ /// <summary>Harmony patches for <see cref="Game1"/> which notify SMAPI for save load stages.</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.")]
- internal class LoadContextPatch : IHarmonyPatch
+ internal class Game1Patcher : BasePatcher
{
/*********
** Fields
@@ -39,42 +35,32 @@ namespace StardewModdingAPI.Patches
/// <summary>Construct an instance.</summary>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onStageChanged">A callback to invoke when the load stage changes.</param>
- public LoadContextPatch(Reflector reflection, Action<LoadStage> onStageChanged)
+ public Game1Patcher(Reflector reflection, Action<LoadStage> onStageChanged)
{
- LoadContextPatch.Reflection = reflection;
- LoadContextPatch.OnStageChanged = onStageChanged;
+ Game1Patcher.Reflection = reflection;
+ Game1Patcher.OnStageChanged = onStageChanged;
}
/// <inheritdoc />
-#if HARMONY_2
- public void Apply(Harmony harmony)
-#else
- public void Apply(HarmonyInstance harmony)
-#endif
+ public override void Apply(Harmony harmony, IMonitor monitor)
{
- // detect CreatedBasicInfo
- harmony.Patch(
- original: AccessTools.Method(typeof(TitleMenu), nameof(TitleMenu.createdNewCharacter)),
- prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_TitleMenu_CreatedNewCharacter))
- );
-
// detect CreatedInitialLocations and SaveAddedLocations
harmony.Patch(
- original: AccessTools.Method(typeof(Game1), nameof(Game1.AddModNPCs)),
- prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_AddModNPCs))
+ original: this.RequireMethod<Game1>(nameof(Game1.AddModNPCs)),
+ prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_AddModNpcs))
);
// detect CreatedLocations, and track IsInLoadForNewGame
harmony.Patch(
- original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)),
- prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_LoadForNewGame)),
- postfix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.After_Game1_LoadForNewGame))
+ original: this.RequireMethod<Game1>(nameof(Game1.loadForNewGame)),
+ prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_LoadForNewGame)),
+ postfix: this.GetHarmonyMethod(nameof(Game1Patcher.After_LoadForNewGame))
);
// detect ReturningToTitle
harmony.Patch(
- original: AccessTools.Method(typeof(Game1), nameof(Game1.CleanupReturningToTitle)),
- prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_CleanupReturningToTitle))
+ original: this.RequireMethod<Game1>(nameof(Game1.CleanupReturningToTitle)),
+ prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_CleanupReturningToTitle))
);
}
@@ -82,25 +68,16 @@ namespace StardewModdingAPI.Patches
/*********
** Private methods
*********/
- /// <summary>Called before <see cref="TitleMenu.createdNewCharacter"/>.</summary>
- /// <returns>Returns whether to execute the original method.</returns>
- /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
- private static bool Before_TitleMenu_CreatedNewCharacter()
- {
- LoadContextPatch.OnStageChanged(LoadStage.CreatedBasicInfo);
- return true;
- }
-
- /// <summary>Called before <see cref="Game1.AddModNPCs"/>.</summary>
+ /// <summary>The method to call before <see cref="Game1.AddModNPCs"/>.</summary>
/// <returns>Returns whether to execute the original method.</returns>
/// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
- private static bool Before_Game1_AddModNPCs()
+ private static bool Before_AddModNpcs()
{
// When this method is called from Game1.loadForNewGame, it happens right after adding the vanilla
// locations but before initializing them.
- if (LoadContextPatch.IsInLoadForNewGame)
+ if (Game1Patcher.IsInLoadForNewGame)
{
- LoadContextPatch.OnStageChanged(LoadContextPatch.IsCreating()
+ Game1Patcher.OnStageChanged(Game1Patcher.IsCreating()
? LoadStage.CreatedInitialLocations
: LoadStage.SaveAddedLocations
);
@@ -109,32 +86,32 @@ namespace StardewModdingAPI.Patches
return true;
}
- /// <summary>Called before <see cref="Game1.CleanupReturningToTitle"/>.</summary>
+ /// <summary>The method to call before <see cref="Game1.CleanupReturningToTitle"/>.</summary>
/// <returns>Returns whether to execute the original method.</returns>
/// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
- private static bool Before_Game1_CleanupReturningToTitle()
+ private static bool Before_CleanupReturningToTitle()
{
- LoadContextPatch.OnStageChanged(LoadStage.ReturningToTitle);
+ Game1Patcher.OnStageChanged(LoadStage.ReturningToTitle);
return true;
}
- /// <summary>Called before <see cref="Game1.loadForNewGame"/>.</summary>
+ /// <summary>The method to call before <see cref="Game1.loadForNewGame"/>.</summary>
/// <returns>Returns whether to execute the original method.</returns>
/// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
- private static bool Before_Game1_LoadForNewGame()
+ private static bool Before_LoadForNewGame()
{
- LoadContextPatch.IsInLoadForNewGame = true;
+ Game1Patcher.IsInLoadForNewGame = true;
return true;
}
- /// <summary>Called after <see cref="Game1.loadForNewGame"/>.</summary>
+ /// <summary>The method to call after <see cref="Game1.loadForNewGame"/>.</summary>
/// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
- private static void After_Game1_LoadForNewGame()
+ private static void After_LoadForNewGame()
{
- LoadContextPatch.IsInLoadForNewGame = false;
+ Game1Patcher.IsInLoadForNewGame = false;
- if (LoadContextPatch.IsCreating())
- LoadContextPatch.OnStageChanged(LoadStage.CreatedLocations);
+ if (Game1Patcher.IsCreating())
+ Game1Patcher.OnStageChanged(LoadStage.CreatedLocations);
}
/// <summary>Get whether the save file is currently being created.</summary>
@@ -142,7 +119,7 @@ namespace StardewModdingAPI.Patches
{
return
(Game1.currentMinigame is Intro) // creating save with intro
- || (Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField<bool>(menu, "transitioningCharacterCreationMenu").GetValue()); // creating save, skipped intro
+ || (Game1.activeClickableMenu is TitleMenu menu && Game1Patcher.Reflection.GetField<bool>(menu, "transitioningCharacterCreationMenu").GetValue()); // creating save, skipped intro
}
}
}
diff --git a/src/SMAPI/Patches/TitleMenuPatcher.cs b/src/SMAPI/Patches/TitleMenuPatcher.cs
new file mode 100644
index 00000000..b4320ce0
--- /dev/null
+++ b/src/SMAPI/Patches/TitleMenuPatcher.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using HarmonyLib;
+using StardewModdingAPI.Enums;
+using StardewModdingAPI.Internal.Patching;
+using StardewValley.Menus;
+
+namespace StardewModdingAPI.Patches
+{
+ /// <summary>Harmony patches for <see cref="TitleMenu"/> which notify SMAPI when a new character was created.</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.")]
+ internal class TitleMenuPatcher : BasePatcher
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>A callback to invoke when the load stage changes.</summary>
+ private static Action<LoadStage> OnStageChanged;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="onStageChanged">A callback to invoke when the load stage changes.</param>
+ public TitleMenuPatcher(Action<LoadStage> onStageChanged)
+ {
+ TitleMenuPatcher.OnStageChanged = onStageChanged;
+ }
+
+ /// <inheritdoc />
+ public override void Apply(Harmony harmony, IMonitor monitor)
+ {
+ harmony.Patch(
+ original: this.RequireMethod<TitleMenu>(nameof(TitleMenu.createdNewCharacter)),
+ prefix: this.GetHarmonyMethod(nameof(TitleMenuPatcher.Before_CreatedNewCharacter))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method to call before <see cref="TitleMenu.createdNewCharacter"/>.</summary>
+ /// <returns>Returns whether to execute the original method.</returns>
+ /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
+ private static bool Before_CreatedNewCharacter()
+ {
+ TitleMenuPatcher.OnStageChanged(LoadStage.CreatedBasicInfo);
+ return true;
+ }
+ }
+}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index e830f799..3249e02f 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -4,6 +4,8 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using StardewModdingAPI.Framework;
+using StardewModdingAPI.Toolkit.Framework;
+using StardewModdingAPI.Toolkit.Serialization.Models;
namespace StardewModdingAPI
{
@@ -31,6 +33,7 @@ namespace StardewModdingAPI
AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve;
Program.AssertGamePresent();
Program.AssertGameVersion();
+ Program.AssertSmapiVersions();
Program.Start(args);
}
catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // don't use EarlyConstants.GameAssemblyName, since we want to check both possible names
@@ -107,8 +110,35 @@ namespace StardewModdingAPI
}
// max version
- else if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion))
+ if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion))
Program.PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.");
+
+ // bitness
+ bool is64BitGame = LowLevelEnvironmentUtility.Is64BitAssembly(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe"));
+#if SMAPI_FOR_WINDOWS_64BIT_HACK
+ if (!is64BitGame)
+ Program.PrintErrorAndExit("Oops! This is the 64-bit version of SMAPI, but you have the 32-bit version of Stardew Valley. You can reinstall SMAPI using its installer to automatically install the correct version of SMAPI.");
+#elif SMAPI_FOR_WINDOWS
+ if (is64BitGame)
+ Program.PrintErrorAndExit("Oops! This is the 32-bit version of SMAPI, but you have the 64-bit version of Stardew Valley. You can reinstall SMAPI using its installer to automatically install the correct version of SMAPI.");
+#endif
+ }
+
+ /// <summary>Assert that the versions of all SMAPI components are correct.</summary>
+ /// <remarks>Players sometimes have mismatched versions (particularly when installed through Vortex), which can cause some very confusing bugs without this check.</remarks>
+ private static void AssertSmapiVersions()
+ {
+ // get SMAPI version without prerelease suffix (since we can't get that from the assembly versions)
+ ISemanticVersion smapiVersion = new SemanticVersion(Constants.ApiVersion.MajorVersion, Constants.ApiVersion.MinorVersion, Constants.ApiVersion.PatchVersion);
+
+ // compare with assembly versions
+ foreach (var type in new[] { typeof(IManifest), typeof(Manifest) })
+ {
+ AssemblyName assemblyName = type.Assembly.GetName();
+ ISemanticVersion assemblyVersion = new SemanticVersion(assemblyName.Version);
+ if (!assemblyVersion.Equals(smapiVersion))
+ Program.PrintErrorAndExit($"Oops! The 'smapi-internal/{assemblyName.Name}.dll' file is version {assemblyVersion} instead of the required {Constants.ApiVersion}. SMAPI doesn't seem to be installed correctly.");
+ }
}
/// <summary>Initialize SMAPI and launch the game.</summary>
diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs
index ae758e9b..ee8a1674 100644
--- a/src/SMAPI/Properties/AssemblyInfo.cs
+++ b/src/SMAPI/Properties/AssemblyInfo.cs
@@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SMAPI.Tests")]
-[assembly: InternalsVisibleTo("ErrorHandler")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index 413d9f33..7d5e7ef9 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -20,7 +20,8 @@
<ItemGroup>
<PackageReference Include="LargeAddressAware" Version="1.0.5" />
- <PackageReference Include="Mono.Cecil" Version="0.11.3" />
+ <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="Platonymous.TMXTile" Version="1.5.8" />
</ItemGroup>
@@ -75,4 +76,5 @@
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
+ <Import Project="..\SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems" Label="Shared" />
</Project>