summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
blob: 45b34556d39dc6d2c2169c2398398b9799065edd (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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using HarmonyLib;

namespace StardewModdingAPI.Framework.Commands
{
    /// <summary>The 'harmony_summary' SMAPI console command.</summary>
    internal class HarmonySummaryCommand : IInternalCommand
    {
        /*********
        ** Accessors
        *********/
        /// <summary>The command name, which the user must type to trigger it.</summary>
        public string Name { get; } = "harmony_summary";

        /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
        public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary <search>\n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method.";


        /*********
        ** Public methods
        *********/
        /// <summary>Handle the console command when it's entered by the user.</summary>
        /// <param name="args">The command arguments.</param>
        /// <param name="monitor">Writes messages to the console.</param>
        public void HandleCommand(string[] args, IMonitor monitor)
        {
            SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.MethodName).ToArray();

            StringBuilder result = new StringBuilder();

            if (!matches.Any())
                result.AppendLine("No current patches match your search.");
            else
            {
                result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:");
                result.AppendLine();
                foreach (var match in matches)
                {
                    result.AppendLine($"   {match.MethodName}");
                    foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key))
                    {
                        var sortedTypes = ownerGroup.Value
                            .OrderBy(p => p switch
                            {
                                PatchType.Prefix => 0,
                                PatchType.Postfix => 1,
                                PatchType.Finalizer => 2,
                                PatchType.Transpiler => 3,
                                _ => 4
                            });

                        result.AppendLine($"      - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})");
                    }
                    result.AppendLine();
                }
            }

            monitor.Log(result.ToString().TrimEnd(), LogLevel.Info);
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Get all current Harmony patches matching any of the given search terms.</summary>
        /// <param name="searchTerms">The search terms to match.</param>
        private IEnumerable<SearchResult> FilterPatches(string[] searchTerms)
        {
            bool hasSearch = searchTerms.Any();
            bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1);

            foreach (var patch in this.GetAllPatches())
            {
                // matches entire patch
                if (IsMatch(patch.MethodDescription))
                {
                    yield return patch;
                    continue;
                }

                // matches individual patchers
                foreach (var pair in patch.PatchTypesByOwner.ToArray())
                {
                    if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString())))
                        patch.PatchTypesByOwner.Remove(pair.Key);
                }

                if (patch.PatchTypesByOwner.Any())
                    yield return patch;
            }
        }

        /// <summary>Get all current Harmony patches.</summary>
        private IEnumerable<SearchResult> GetAllPatches()
        {
            foreach (MethodBase method in Harmony.GetAllPatchedMethods().ToArray())
            {
                // get metadata for method
                HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method);

                IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>>
                {
                    [PatchType.Prefix] = patchInfo.Prefixes,
                    [PatchType.Postfix] = patchInfo.Postfixes,
                    [PatchType.Finalizer] = patchInfo.Finalizers,
                    [PatchType.Transpiler] = patchInfo.Transpilers
                };

                // get patch types by owner
                var typesByOwner = new Dictionary<string, ISet<PatchType>>();
                foreach (var group in patchGroups)
                {
                    foreach (var patch in group.Value)
                    {
                        if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType> patchTypes))
                            typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>();
                        patchTypes.Add(group.Key);
                    }
                }

                // create search result
                yield return new SearchResult(method, typesByOwner);
            }
        }

        /// <summary>A Harmony patch type.</summary>
        private enum PatchType
        {
            /// <summary>A prefix patch.</summary>
            Prefix,

            /// <summary>A postfix patch.</summary>
            Postfix,

            /// <summary>A finalizer patch.</summary>
            Finalizer,

            /// <summary>A transpiler patch.</summary>
            Transpiler
        }

        /// <summary>A patch search result for a method.</summary>
        private class SearchResult
        {
            /*********
            ** Accessors
            *********/
            /// <summary>A simple human-readable name for the patched method.</summary>
            public string MethodName { get; }

            /// <summary>A detailed description for the patched method.</summary>
            public string MethodDescription { get; }

            /// <summary>The patch types by the Harmony instance ID that added them.</summary>
            public IDictionary<string, ISet<PatchType>> PatchTypesByOwner { get; }


            /*********
            ** Public methods
            *********/
            /// <summary>Construct an instance.</summary>
            /// <param name="method">The patched method.</param>
            /// <param name="patchTypesByOwner">The patch types by the Harmony instance ID that added them.</param>
            public SearchResult(MethodBase method, IDictionary<string, ISet<PatchType>> patchTypesByOwner)
            {
                this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}";
                this.MethodDescription = method.FullDescription();
                this.PatchTypesByOwner = patchTypesByOwner;
            }
        }
    }
}