Skip to content

Commit b98a7d3

Browse files
committed
Initial project commit
1 parent ab82248 commit b98a7d3

File tree

11 files changed

+1995
-0
lines changed

11 files changed

+1995
-0
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Sybaris Loader for [UnityDoorstop](https://github.com/NeighTools/UnityDoorstop)
2+
3+
> ⚠️ **THIS IS A LEGACY SOFTWARE FOR TESTING PURPOSES ONLY** ⚠️
4+
>
5+
> This software exists as an example of how to use [UnityDoorstop](https://github.com/NeighTools/UnityDoorstop)
6+
> to preload managed code during Unity startup.
7+
>
8+
> Use for testing purposes only!
9+
> For a stable version, please use [BepInEx](https://github.com/bbepis/BepInEx) when a suitable loader exists.
10+
11+
This is a simple loader that loads and applies Sybaris-compatible patchers without editing the assembly files.
12+
13+
## Requirements
14+
15+
* [UnityDoorstop 2.0](https://github.com/NeighTools/UnityDoorstop) or higher
16+
17+
## Installation
18+
19+
1. Extract the contents of the archive into `UnityDoorstop` folder. Overwrite when asked.
20+
21+
## Building
22+
23+
You can build with Visual Studio 2015/2017 with .NET 3.5 installed. All dependencies are handled by NuGet.

SybarisLoader.sln

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.27428.2043
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SybarisLoader", "SybarisLoader\SybarisLoader.csproj", "{55346E6C-F272-47AA-A1ED-027D5D8251D9}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{55346E6C-F272-47AA-A1ED-027D5D8251D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{55346E6C-F272-47AA-A1ED-027D5D8251D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{55346E6C-F272-47AA-A1ED-027D5D8251D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{55346E6C-F272-47AA-A1ED-027D5D8251D9}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {2DF731F7-D29E-486B-BC14-4AF3E03BE6FD}
24+
EndGlobalSection
25+
EndGlobal

SybarisLoader/Configuration.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.IO;
3+
using System.Text;
4+
using SimpleJSON;
5+
using SybarisLoader.Util;
6+
7+
namespace PatchLoader
8+
{
9+
public static class Configuration
10+
{
11+
public static JSONNode Options { get; private set; }
12+
13+
public static void Init()
14+
{
15+
string configFile = Path.Combine(Utils.SybarisDir, "SybarisLoader.json");
16+
17+
if (!File.Exists(configFile))
18+
{
19+
InitDefaultConfig(configFile);
20+
return;
21+
}
22+
23+
try
24+
{
25+
Options = JSON.Parse(File.ReadAllText(configFile, Encoding.UTF8));
26+
}
27+
catch (Exception)
28+
{
29+
Logger.Log(LogLevel.Warning, $"Failed to load configuration file {configFile}! Creating one!");
30+
InitDefaultConfig(configFile);
31+
}
32+
}
33+
34+
private static void InitDefaultConfig(string path)
35+
{
36+
Options = new JSONObject();
37+
38+
Options["debug"]["logging"]["enabled"] = true;
39+
Options["debug"]["logging"]["redirectConsole"] = true;
40+
Options["debug"]["outputAssemblies"]["enabled"] = false;
41+
Options["debug"]["outputAssemblies"]["outputDirectory"] = @"UnityPrePatcher\debug\assemblies";
42+
43+
StringBuilder sb = new StringBuilder();
44+
45+
Options.WriteToStringBuilder(sb, 2, 2, JSONTextMode.Indent);
46+
47+
try
48+
{
49+
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
50+
}
51+
catch (Exception e)
52+
{
53+
Logger.Log(LogLevel.Warning, $"Failed to save configuration file to {path}!\nReason: {e.Message}");
54+
}
55+
}
56+
}
57+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Reflection;
2+
using System.Runtime.InteropServices;
3+
4+
// General Information about an assembly is controlled through the following
5+
// set of attributes. Change these attribute values to modify the information
6+
// associated with an assembly.
7+
[assembly: AssemblyTitle("SybarisLoader")]
8+
[assembly: AssemblyDescription("")]
9+
[assembly: AssemblyConfiguration("")]
10+
[assembly: AssemblyCompany("")]
11+
[assembly: AssemblyProduct("SybarisLoader")]
12+
[assembly: AssemblyCopyright("Copyright © 2018")]
13+
[assembly: AssemblyTrademark("")]
14+
[assembly: AssemblyCulture("")]
15+
16+
// Setting ComVisible to false makes the types in this assembly not visible
17+
// to COM components. If you need to access a type in this assembly from
18+
// COM, set the ComVisible attribute to true on that type.
19+
[assembly: ComVisible(false)]
20+
21+
// The following GUID is for the ID of the typelib if this project is exposed to COM
22+
[assembly: Guid("55346e6c-f272-47aa-a1ed-027d5d8251d9")]
23+
24+
// Version information for an assembly consists of the following four values:
25+
//
26+
// Major Version
27+
// Minor Version
28+
// Build Number
29+
// Revision
30+
//
31+
// You can specify all the values or you can default the Build and Revision Numbers
32+
// by using the '*' as shown below:
33+
// [assembly: AssemblyVersion("1.0.*")]
34+
[assembly: AssemblyVersion("1.0.0.0")]
35+
[assembly: AssemblyFileVersion("1.0.0.0")]

SybarisLoader/SybarisLoader.cs

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Reflection;
5+
using Mono.Cecil;
6+
using PatchLoader;
7+
using SybarisLoader.Util;
8+
9+
namespace SybarisLoader
10+
{
11+
/// <summary>
12+
/// The entry point class of patch loader.
13+
/// </summary>
14+
/// <remarks>
15+
/// At the moment this loader requires to System.dll being loaded into memroy to work, which is why it cannot be
16+
/// patched with this method.
17+
/// </remarks>
18+
public static class Loader
19+
{
20+
private static Dictionary<string, List<MethodInfo>> patchersDictionary;
21+
22+
public static void LoadPatchers()
23+
{
24+
patchersDictionary = new Dictionary<string, List<MethodInfo>>();
25+
26+
Logger.Log(LogLevel.Info, "Loading patchers");
27+
28+
foreach (string dll in Directory.GetFiles(Utils.PatchesDir, "*.Patcher.dll"))
29+
{
30+
Assembly assembly;
31+
32+
try
33+
{
34+
assembly = Assembly.LoadFile(dll);
35+
}
36+
catch (Exception e)
37+
{
38+
Logger.Log(LogLevel.Error, $"Failed to load {dll}: {e.Message}");
39+
if (e.InnerException != null)
40+
Logger.Log(LogLevel.Error, $"Inner: {e.InnerException}");
41+
continue;
42+
}
43+
44+
foreach (Type type in assembly.GetTypes())
45+
{
46+
if (type.IsInterface)
47+
continue;
48+
49+
FieldInfo targetAssemblyNamesField =
50+
type.GetField("TargetAssemblyNames", BindingFlags.Static | BindingFlags.Public);
51+
52+
if (targetAssemblyNamesField == null || targetAssemblyNamesField.FieldType != typeof(string[]))
53+
continue;
54+
55+
MethodInfo patchMethod = type.GetMethod("Patch", BindingFlags.Static | BindingFlags.Public);
56+
57+
if (patchMethod == null || patchMethod.ReturnType != typeof(void))
58+
continue;
59+
60+
ParameterInfo[] parameters = patchMethod.GetParameters();
61+
62+
if (parameters.Length != 1 || parameters[0].ParameterType != typeof(AssemblyDefinition))
63+
continue;
64+
65+
string[] requestedAssemblies = targetAssemblyNamesField.GetValue(null) as string[];
66+
67+
if (requestedAssemblies == null || requestedAssemblies.Length == 0)
68+
continue;
69+
70+
Logger.Log(LogLevel.Info, $"Adding {type.FullName}");
71+
72+
foreach (string requestedAssembly in requestedAssemblies)
73+
{
74+
if (!patchersDictionary.TryGetValue(requestedAssembly, out List<MethodInfo> list))
75+
{
76+
list = new List<MethodInfo>();
77+
patchersDictionary.Add(requestedAssembly, list);
78+
}
79+
80+
list.Add(patchMethod);
81+
}
82+
}
83+
}
84+
}
85+
86+
/// <summary>
87+
/// Carry out patching on the asemblies.
88+
/// </summary>
89+
public static void Patch()
90+
{
91+
Logger.Log(LogLevel.Info, "Patching assemblies:");
92+
93+
foreach (KeyValuePair<string, List<MethodInfo>> patchJob in patchersDictionary)
94+
{
95+
string assemblyName = patchJob.Key;
96+
List<MethodInfo> patchers = patchJob.Value;
97+
98+
string assemblyPath = Path.Combine(Utils.GameAssembliesDir, assemblyName);
99+
100+
if (!File.Exists(assemblyPath))
101+
{
102+
Logger.Log(LogLevel.Warning, $"{assemblyPath} does not exist. Skipping...");
103+
continue;
104+
}
105+
106+
AssemblyDefinition assemblyDefinition;
107+
108+
try
109+
{
110+
assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath);
111+
}
112+
catch (Exception e)
113+
{
114+
Logger.Log(LogLevel.Error, $"Failed to open {assemblyPath}: {e.Message}");
115+
continue;
116+
}
117+
118+
foreach (MethodInfo patcher in patchers)
119+
{
120+
Logger.Log(LogLevel.Info, $"Running {patcher.DeclaringType.FullName}");
121+
try
122+
{
123+
patcher.Invoke(null, new object[] {assemblyDefinition});
124+
}
125+
catch (TargetInvocationException te)
126+
{
127+
Exception inner = te.InnerException;
128+
if (inner != null)
129+
{
130+
Logger.Log(LogLevel.Error, $"Error inside the patcher: {inner.Message}");
131+
Logger.Log(LogLevel.Error, $"Stack trace:\n{inner.StackTrace}");
132+
}
133+
}
134+
catch (Exception e)
135+
{
136+
Logger.Log(LogLevel.Error, $"By the patcher loader: {e.Message}");
137+
Logger.Log(LogLevel.Error, $"Stack trace:\n{e.StackTrace}");
138+
}
139+
}
140+
141+
MemoryStream ms = new MemoryStream();
142+
143+
// Write the patched assembly into memory
144+
assemblyDefinition.Write(ms);
145+
assemblyDefinition.Dispose();
146+
147+
byte[] assemblyBytes = ms.ToArray();
148+
149+
// Save the patched assembly to a file for debugging purposes
150+
SavePatchedAssembly(assemblyBytes, Path.GetFileNameWithoutExtension(assemblyName));
151+
152+
// Load the patched assembly directly from memory
153+
// Since .NET loads all assemblies only once,
154+
// any further attempts by Unity to load the patched assemblies
155+
// will do nothing. Thus we achieve the same "dynamic patching" effect as with Sybaris.
156+
Assembly.Load(ms.ToArray());
157+
158+
ms.Dispose();
159+
}
160+
}
161+
162+
/// <summary>
163+
/// The entry point of the loader
164+
/// </summary>
165+
public static void Main()
166+
{
167+
if (!Directory.Exists(Utils.SybarisDir))
168+
Directory.CreateDirectory(Utils.SybarisDir);
169+
if (!Directory.Exists(Utils.LogsDir))
170+
Directory.CreateDirectory(Utils.LogsDir);
171+
172+
Configuration.Init();
173+
174+
if (Configuration.Options["debug"]["logging"]["enabled"])
175+
Logger.Enabled = true;
176+
if (Configuration.Options["debug"]["logging"]["redirectConsole"])
177+
Logger.RerouteStandardIO();
178+
179+
Logger.Log("===Sybaris Loader===");
180+
Logger.Log($"Started on {DateTime.Now:R}");
181+
Logger.Log($"Game assembly directory: {Utils.GameAssembliesDir}");
182+
Logger.Log($"Doorstop directory: {Utils.RootDir}");
183+
184+
if (!Directory.Exists(Utils.PatchesDir))
185+
{
186+
Directory.CreateDirectory(Utils.PatchesDir);
187+
Logger.Log(LogLevel.Info, "No patches directory found! Created an empty one!");
188+
Logger.Dispose();
189+
return;
190+
}
191+
192+
Logger.Log(LogLevel.Info, "Adding ResolveAssembly Handler");
193+
194+
// We add a custom assembly resolver
195+
// Since assemblies don't unload, this event handler will be called always there is an assembly to resolve
196+
// This allows us to put our patchers and plug-ins in our own folders.
197+
AppDomain.CurrentDomain.AssemblyResolve += ResolvePatchers;
198+
199+
LoadPatchers();
200+
201+
if (patchersDictionary.Count == 0)
202+
{
203+
Logger.Log(LogLevel.Info, "No valid patchers found! Quiting...");
204+
Logger.Dispose();
205+
return;
206+
}
207+
208+
Patch();
209+
210+
Logger.Log(LogLevel.Info, "Patching complete! Disposing of logger!");
211+
Logger.Dispose();
212+
}
213+
214+
public static Assembly ResolvePatchers(object sender, ResolveEventArgs args)
215+
{
216+
// Try to resolve from patches directory
217+
if (Utils.TryResolveDllAssembly(args.Name, Utils.PatchesDir, out Assembly patchAssembly))
218+
return patchAssembly;
219+
return null;
220+
}
221+
222+
private static void SavePatchedAssembly(byte[] assembly, string name)
223+
{
224+
if (!Configuration.Options["debug"]["outputAssemblies"]["enabled"]
225+
|| !Configuration.Options["debug"]["outputAssemblies"]["outputDirectory"].IsString
226+
|| Configuration.Options["debug"]["outputAssemblies"]["outputDirectory"] == null)
227+
return;
228+
229+
string outDir = Configuration.Options["debug"]["outputAssemblies"]["outputDirectory"];
230+
231+
string path = Path.Combine(outDir, $"{name}_patched.dll");
232+
233+
if (!Directory.Exists(outDir))
234+
try
235+
{
236+
Directory.CreateDirectory(outDir);
237+
}
238+
catch (Exception e)
239+
{
240+
Logger.Log(LogLevel.Warning,
241+
$"Failed to create patched assembly directory to {outDir}!\nReason: {e.Message}");
242+
return;
243+
}
244+
245+
File.WriteAllBytes(path, assembly);
246+
247+
Logger.Log(LogLevel.Info, $"Saved patched {name} to {path}");
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)