Skip to content

Commit 4010d2c

Browse files
authored
ScriptEngine: Add assembly dumping setting for debugging of reloaded plugins (#15)
Similarly to the LoadDumpedAssemblies BepInEx Preloader option, this adds a config option which dumps Assemblies & Symbols to the disk. The assemblies are then loaded from the saved dll so that debuggers can then load the matching symbols & break/step through plugin code. Tested successfully on Rider but should work for all debuggers that are configured to read symbols from the BepInEx folder.
1 parent 1a07941 commit 4010d2c

File tree

1 file changed

+76
-41
lines changed

1 file changed

+76
-41
lines changed

src/ScriptEngine/ScriptEngine.cs

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public class ScriptEngine : BaseUnityPlugin
3333
private ConfigEntry<bool> IncludeSubdirectories { get; set; }
3434
private ConfigEntry<float> AutoReloadDelay { get; set; }
3535

36+
private ConfigEntry<bool> DumpAssemblies { get; set; }
37+
private static readonly string DumpedAssembliesPath = Utility.CombinePaths(Paths.BepInExRootPath, "ScriptEngineDumpedAssemblies");
38+
3639
private FileSystemWatcher fileSystemWatcher;
3740
private bool shouldReload;
3841
private float autoReloadTimer;
@@ -45,6 +48,10 @@ private void Awake()
4548
IncludeSubdirectories = Config.Bind("General", "IncludeSubdirectories", false, new ConfigDescription("Also load plugins from subdirectories of the scripts folder."));
4649
EnableFileSystemWatcher = Config.Bind("AutoReload", "EnableFileSystemWatcher", false, new ConfigDescription("Watches the scripts directory for file changes and automatically reloads all plugins if any of the files gets changed (added/removed/modified)."));
4750
AutoReloadDelay = Config.Bind("AutoReload", "AutoReloadDelay", 3.0f, new ConfigDescription("Delay in seconds from detecting a change to files in the scripts directory to plugins being reloaded. Affects only EnableFileSystemWatcher."));
51+
DumpAssemblies = Config.Bind<bool>("AutoReload", "DumpAssemblies", false, "If enabled, BepInEx will save patched assemblies & symbols into BepInEx/ScriptEngineDumpedAssemblies.\nThis can be used by developers to inspect and debug plugins loaded by ScriptEngine.");
52+
53+
if (Directory.Exists(DumpedAssembliesPath))
54+
Directory.Delete(DumpedAssembliesPath, true);
4855

4956
if (LoadOnStart.Value)
5057
ReloadPlugins();
@@ -114,60 +121,88 @@ private void LoadDLL(string path, GameObject obj)
114121
if (!QuietMode.Value)
115122
Logger.Log(LogLevel.Info, $"Loading plugins from {path}");
116123

117-
using (var dll = AssemblyDefinition.ReadAssembly(path, new ReaderParameters { AssemblyResolver = defaultResolver }))
124+
using (var dll = AssemblyDefinition.ReadAssembly(path, new ReaderParameters {
125+
AssemblyResolver = defaultResolver,
126+
ReadSymbols = true
127+
}))
118128
{
119129
dll.Name.Name = $"{dll.Name.Name}-{DateTime.Now.Ticks}";
130+
Assembly ass;
120131

121-
using (var ms = new MemoryStream())
132+
if (DumpAssemblies.Value)
122133
{
123-
dll.Write(ms);
124-
var ass = Assembly.Load(ms.ToArray());
134+
// Dump assembly & load it from disk
135+
if (!Directory.Exists(DumpedAssembliesPath))
136+
Directory.CreateDirectory(DumpedAssembliesPath);
137+
138+
string assemblyDumpPath = Path.Combine(DumpedAssembliesPath, dll.Name.Name + Path.GetExtension(dll.MainModule.Name));
125139

126-
foreach (Type type in GetTypesSafe(ass))
140+
using (FileStream outFileStream = new FileStream(assemblyDumpPath, FileMode.Create))
127141
{
128-
try
142+
dll.Write((Stream)outFileStream, new WriterParameters()
129143
{
130-
if (!typeof(BaseUnityPlugin).IsAssignableFrom(type)) continue;
144+
WriteSymbols = true
145+
});
146+
}
131147

132-
var metadata = MetadataHelper.GetMetadata(type);
133-
if (metadata == null) continue;
148+
ass = Assembly.LoadFile(assemblyDumpPath);
149+
if (!QuietMode.Value)
150+
Logger.Log(LogLevel.Info, $"Loaded dumped Assembly from {assemblyDumpPath}");
151+
} else
152+
{
153+
// Load from memory
154+
using (var ms = new MemoryStream())
155+
{
156+
dll.Write(ms);
157+
ass = Assembly.Load(ms.ToArray());
158+
}
159+
}
134160

135-
if (!QuietMode.Value)
136-
Logger.Log(LogLevel.Info, $"Loading {metadata.GUID}");
137161

138-
if (Chainloader.PluginInfos.TryGetValue(metadata.GUID, out var existingPluginInfo))
139-
throw new InvalidOperationException($"A plugin with GUID {metadata.GUID} is already loaded! ({existingPluginInfo.Metadata.Name} v{existingPluginInfo.Metadata.Version})");
162+
foreach (Type type in GetTypesSafe(ass))
163+
{
164+
try
165+
{
166+
if (!typeof(BaseUnityPlugin).IsAssignableFrom(type)) continue;
140167

141-
var typeDefinition = dll.MainModule.Types.First(x => x.FullName == type.FullName);
142-
var pluginInfo = Chainloader.ToPluginInfo(typeDefinition);
168+
var metadata = MetadataHelper.GetMetadata(type);
169+
if (metadata == null) continue;
143170

144-
StartCoroutine(DelayAction(() =>
145-
{
146-
try
147-
{
148-
// Need to add to PluginInfos first because BaseUnityPlugin constructor (called by AddComponent below)
149-
// looks in PluginInfos for an existing PluginInfo and uses it instead of creating a new one.
150-
Chainloader.PluginInfos[metadata.GUID] = pluginInfo;
151-
152-
var instance = obj.AddComponent(type);
153-
154-
// Fill in properties that are normally set by Chainloader
155-
var tv = Traverse.Create(pluginInfo);
156-
tv.Property<BaseUnityPlugin>(nameof(pluginInfo.Instance)).Value = (BaseUnityPlugin)instance;
157-
// Loading the assembly from memory causes Location to be lost
158-
tv.Property<string>(nameof(pluginInfo.Location)).Value = path;
159-
}
160-
catch (Exception e)
161-
{
162-
Logger.LogError($"Failed to load plugin {metadata.GUID} because of exception: {e}");
163-
Chainloader.PluginInfos.Remove(metadata.GUID);
164-
}
165-
}));
166-
}
167-
catch (Exception e)
171+
if (!QuietMode.Value)
172+
Logger.Log(LogLevel.Info, $"Loading {metadata.GUID}");
173+
174+
if (Chainloader.PluginInfos.TryGetValue(metadata.GUID, out var existingPluginInfo))
175+
throw new InvalidOperationException($"A plugin with GUID {metadata.GUID} is already loaded! ({existingPluginInfo.Metadata.Name} v{existingPluginInfo.Metadata.Version})");
176+
177+
var typeDefinition = dll.MainModule.Types.First(x => x.FullName == type.FullName);
178+
var pluginInfo = Chainloader.ToPluginInfo(typeDefinition);
179+
180+
StartCoroutine(DelayAction(() =>
168181
{
169-
Logger.LogError($"Failed to load plugin {type.Name} because of exception: {e}");
170-
}
182+
try
183+
{
184+
// Need to add to PluginInfos first because BaseUnityPlugin constructor (called by AddComponent below)
185+
// looks in PluginInfos for an existing PluginInfo and uses it instead of creating a new one.
186+
Chainloader.PluginInfos[metadata.GUID] = pluginInfo;
187+
188+
var instance = obj.AddComponent(type);
189+
190+
// Fill in properties that are normally set by Chainloader
191+
var tv = Traverse.Create(pluginInfo);
192+
tv.Property<BaseUnityPlugin>(nameof(pluginInfo.Instance)).Value = (BaseUnityPlugin)instance;
193+
// Loading the assembly from memory causes Location to be lost
194+
tv.Property<string>(nameof(pluginInfo.Location)).Value = path;
195+
}
196+
catch (Exception e)
197+
{
198+
Logger.LogError($"Failed to load plugin {metadata.GUID} because of exception: {e}");
199+
Chainloader.PluginInfos.Remove(metadata.GUID);
200+
}
201+
}));
202+
}
203+
catch (Exception e)
204+
{
205+
Logger.LogError($"Failed to load plugin {type.Name} because of exception: {e}");
171206
}
172207
}
173208
}

0 commit comments

Comments
 (0)