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 } !\n Reason: { 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