-
Notifications
You must be signed in to change notification settings - Fork 0
/
ModInjector.cs
239 lines (204 loc) · 9.64 KB
/
ModInjector.cs
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using BuildLog = HarmonyInjector.BuildLog;
using InjectionException = HarmonyInjector.InjectionException;
using StaticInitializationException = HarmonyInjector.StaticInitializationException;
using XRLCore = XRL.Core.XRLCore;
using static HarmonyInjector.Tools;
using static HarmonyInjector.Constants;
namespace XRL.World.Parts
{
public class ModInjector : IPart
{
/// <summary>
/// Will be true when running in an injected context.
/// See `HarmonyInjector.Constants.FauxModInjectorCode` to see the version
/// of this class that is used by the injected context.
/// </summary>
public static bool InjectedHarmony => false;
/// <summary>
/// Indicates whether an injection attempt is currently taking place.
/// </summary>
public static bool CurrentlyInjecting { get; private set; } = false;
private static ModInfo InjectorModInfo
{
get
{
var modInfo = ModManager.Mods.FirstOrDefault(m => m.ID == ModID && m.IsApproved);
if (modInfo != null) return modInfo;
throw new InjectionException($"The `ModInfo` entry for `{ModID}` could not be located or was not approved.");
}
}
private static string InjectorModPath
{
get
{
// Find the directory containing this mod.
var directory = InjectorModInfo.Path;
if (!String.IsNullOrEmpty(directory) && Directory.Exists(directory)) return directory;
throw new DirectoryNotFoundException($"Directory containing {ModID} was not found.");
}
}
private static string HarmonyAssemblyLocation
{
get
{
// Load `0Harmony.dll` from this mod's directory.
var location = Directory
.GetFiles(InjectorModPath)
.FirstOrDefault(path => CompareStrings(Path.GetFileName(path), HarmonyDll));
if (!String.IsNullOrEmpty(location)) return location;
throw new FileNotFoundException("Harmony DLL was not found.");
}
}
public ModInjector()
{
// Only attempt injection once.
if (ranOnce) return;
ranOnce = true;
// Inform an injection attempt is taking place.
BuildLog.Info("Injecting Harmony...");
CurrentlyInjecting = true;
// Queue the injection on the UI-thread. If initialization using the previous
// mod assembly is still occuring, this will wait for it to complete before we
// start mucking things up.
QueueUITask(() => WaitFor(() => GameIsReady, DoInject, 0));
}
private static void DoInject()
{
// Cache the previous assembly. It can't be unloaded and isn't going anywhere.
var previousAssembly = ModManager.modAssembly;
try
{
// Once the harmony assembly is loaded into the app-domain, Caves of Qud
// will automatically reference it in the mod assembly.
BuildLog.Info("Pulling the Harmony assembly into the current AppDomain.");
var harmonyAssembly = Assembly.LoadFrom(HarmonyAssemblyLocation);
// Add an `AppDomain.AssemblyResolve` handler that matches the mod assembly.
// Harmony will need this to successfully perform un-patching.
AppDomain.CurrentDomain.AssemblyResolve += (obj, args) =>
args.Name == ModManager.modAssembly?.FullName ? ModManager.modAssembly : null;
BuildLog.Info("Rebuilding mods into a Harmony-injected assembly.");
// Refresh the mods; this rebuilds `ModManager.Mods`, where we can then alter
// this mod's source code into the "Harmony-injected" version.
ModManager.Refresh();
BuildLog.Info($"Adjusting the scripts of {ModID} for the injected mod assembly.");
AdjustModForInjection();
ModManager.BuildScriptMods();
// Check to make sure the mod assembly has updated.
if (ModManager.modAssembly == null)
throw new InjectionException($"The Harmony-injected mod assembly failed to build.");
if (ModManager.modAssembly == typeof(ModInjector).Assembly)
throw new InjectionException($"The mod assembly still points to this assembly; this is not expected.");
BuildLog.Info("Harmony-injected mod assembly built successfully.");
BuildLog.Info("Calling static constructors in the mod assembly...");
RunStaticConstructors();
// A config reload ensures the game begins to use the types from the rebuilt assembly.
BuildLog.Info("Reloading the game configuration...");
XRLCore.Core.HotloadConfiguration();
BuildLog.Info("Injection successful.");
}
catch (InjectionException ex)
{
BuildLog.Error(ex.Message);
BuildLog.Error("Injection failed.");
ShowErrorPopup(
"&YHarmony could not be injected properly.",
$"&R{ex.Message}"
);
}
catch (StaticInitializationException ex)
{
// The mod assembly built successfully, but it is having trouble initializing.
BuildLog.Error(ex.Message);
BuildLog.Error(ex.InnerException);
BuildLog.Error("Injection failed.");
// Restore the old mod assembly.
ModManager.modAssembly = previousAssembly;
// We can't really know what mod failed, but hopefully we can give enough
// information such that it is obvious to the player.
var lastEx = EnumerateExceptions(ex.InnerException).Last();
ShowErrorPopup(
$"&YStatic construction of &W{ex.TargetType.FullName}&Y failed.",
$"&wError &W{lastEx.GetType().Name}",
$"&R{lastEx.Message}",
TraceThrough(ex.InnerException).Select((s, i) => $"&w{(i > 0 ? "Via" : "At")} &W{s}")
);
}
catch (Exception ex)
{
BuildLog.Error("An unexpected exception was raised during the injection process.");
BuildLog.Error(ex);
BuildLog.Error("Injection failed.");
ShowErrorPopup(
"&YHarmony could not be injected properly.",
"&RAn unhandled error occurred during the injection process."
);
}
finally
{
// Restore the previous assembly, in case of a failure, just in case
// some kind of error recovery triggers and Qud still expects mod-types
// to be retrievable.
if (ModManager.modAssembly == null)
{
ModManager.modAssembly = previousAssembly;
ModManager.bCompiled = true;
}
CurrentlyInjecting = false;
}
}
private static IEnumerable<string> TraceThrough(Exception exception) =>
EnumerateExceptions(exception).Reverse()
.Select(ex => $"{ex.TargetSite.DeclaringType.FullName}::{ex.TargetSite.Name}");
private static void ShowErrorPopup(params object[] msgParts)
{
var sb = new StringBuilder();
foreach (var part in EnumerateStrings(msgParts)) sb.AppendLine(part);
sb.AppendLine();
sb.AppendLine("&YSome mods may not function correctly.");
sb.Append("Check the output logs for more information.");
HarmonyInjector.Popup.ShowMessage(sb.ToString());
}
private static void AdjustModForInjection()
{
var modInfo = InjectorModInfo;
// Make a few changes to this mod's script files before rebuilding the mod assembly.
// Since we're changing the collection in the loop, we'll copy it to an array first.
foreach (var path in modInfo.ScriptFiles.ToArray())
{
switch (Path.GetFileName(path))
{
// Remove sources that are not utilized by the injected version.
case "Exceptions.cs":
case "HarmonyInterface.cs":
modInfo.ScriptFiles.Remove(path);
modInfo.ScriptFileContents.Remove(path);
break;
// Replace with a simplified version of `XRL.World.Parts.ModInjector` to keep Qud happy.
case "ModInjector.cs":
modInfo.ScriptFileContents[path] = FauxModInjectorCode;
break;
}
}
}
private static void RunStaticConstructors()
{
foreach (var type in ModManager.modAssembly.GetTypes())
{
try { RuntimeHelpers.RunClassConstructor(type.TypeHandle); }
catch (Exception ex)
{
// Wrap and throw the exception.
throw new StaticInitializationException(type, ex);
}
}
}
private static bool ranOnce = false;
}
}