using System; using HarmonyLib; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Framework.ModLoading.RewriteFacades; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Detects Harmony references, and rewrites Harmony 1.x assembly references to work with Harmony 2.x. internal class HarmonyRewriter : BaseInstructionHandler { /********* ** Fields *********/ /// Whether any Harmony 1.x types were replaced. private bool ReplacedTypes; /// Whether to rewrite Harmony 1.x code. private readonly bool ShouldRewrite; /********* ** Public methods *********/ /// Construct an instance. public HarmonyRewriter(bool shouldRewrite = true) : base(defaultPhrase: "Harmony 1.x") { this.ShouldRewrite = shouldRewrite; } /// public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { // detect Harmony if (type.Scope is not AssemblyNameReference scope || scope.Name != "0Harmony") return false; // rewrite Harmony 1.x type to Harmony 2.0 type if (this.ShouldRewrite && scope.Version.Major == 1) { Type targetType = this.GetMappedType(type); replaceWith(module.ImportReference(targetType)); this.OnChanged(); this.ReplacedTypes = true; return true; } this.MarkFlag(InstructionHandleResult.DetectedGamePatch); return false; } /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { if (this.ShouldRewrite) { // 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; } /********* ** Private methods *********/ /// Update the mod metadata when any Harmony 1.x code is migrated. private void OnChanged() { this.MarkRewritten(); this.MarkFlag(InstructionHandleResult.DetectedGamePatch); } /// Rewrite methods to use Harmony facades if needed. /// The assembly module containing the method reference. /// The method reference to map. private bool TryRewriteMethodsToFacade(ModuleDefinition module, MethodReference? methodRef) { if (!this.ReplacedTypes) return false; // not Harmony (or already using Harmony 2.0) // get facade type Type? toType = methodRef?.DeclaringType.FullName switch { "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!)) { methodRef!.DeclaringType = module.ImportReference(toType); return true; } return false; } /// Get an equivalent Harmony 2.x type. /// The Harmony 1.x type. private Type GetMappedType(TypeReference type) { return type.FullName switch { "Harmony.HarmonyInstance" => typeof(Harmony), "Harmony.ILCopying.ExceptionBlock" => typeof(ExceptionBlock), _ => this.GetMappedTypeByConvention(type) }; } /// Get an equivalent Harmony 2.x type using the convention expected for most types. /// The Harmony 1.x type. private Type GetMappedTypeByConvention(TypeReference type) { string fullName = type.FullName.Replace("Harmony.", "HarmonyLib."); string targetName = typeof(Harmony).AssemblyQualifiedName!.Replace(typeof(Harmony).FullName!, fullName); return Type.GetType(targetName, throwOnError: true)!; } } }