summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs
blob: 92397c58b47240760a02529e5098f32e11eb447e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#nullable disable

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
{
    /// <summary>Detects Harmony references, and rewrites Harmony 1.x assembly references to work with Harmony 2.x.</summary>
    internal class HarmonyRewriter : BaseInstructionHandler
    {
        /*********
        ** Fields
        *********/
        /// <summary>Whether any Harmony 1.x types were replaced.</summary>
        private bool ReplacedTypes;

        /// <summary>Whether to rewrite Harmony 1.x code.</summary>
        private readonly bool ShouldRewrite;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        public HarmonyRewriter(bool shouldRewrite = true)
            : base(defaultPhrase: "Harmony 1.x")
        {
            this.ShouldRewrite = shouldRewrite;
        }

        /// <inheritdoc />
        public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> 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;
        }

        /// <inheritdoc />
        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
        *********/
        /// <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>
        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;
        }

        /// <summary>Get an equivalent Harmony 2.x type.</summary>
        /// <param name="type">The Harmony 1.x type.</param>
        private Type GetMappedType(TypeReference type)
        {
            return type.FullName switch
            {
                "Harmony.HarmonyInstance" => typeof(Harmony),
                "Harmony.ILCopying.ExceptionBlock" => typeof(ExceptionBlock),
                _ => this.GetMappedTypeByConvention(type)
            };
        }

        /// <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);
            return Type.GetType(targetName, throwOnError: true);
        }
    }
}