99using System . Text ;
1010using System . Text . RegularExpressions ;
1111
12- namespace Bannerlord . ModuleLoader . Injector
12+ namespace Bannerlord . ModuleLoader . Injector ;
13+
14+ [ Generator ]
15+ public class InjectorGenerator : IIncrementalGenerator
1316{
14- [ Generator ]
15- public class InjectorGenerator : IIncrementalGenerator
17+ private readonly record struct BuildProperties ( string ? OutputPath , string ? CsProjPath , string ? ModuleId ) ;
18+
19+ private static readonly DiagnosticDescriptor MissingRequiredBuildProperties = new (
20+ "INJ001" ,
21+ "Missing Required Build Properties" ,
22+ "OutputPath, MSBuildProjectFullPath, or ModuleId not provided. Skipping generation." ,
23+ "Generator" ,
24+ DiagnosticSeverity . Warning ,
25+ true ) ;
26+
27+ private static readonly DiagnosticDescriptor StartingModuleGeneration = new (
28+ "INJ002" ,
29+ "Starting Module Generation" ,
30+ "Generating module files for {0}..." ,
31+ "Generator" ,
32+ DiagnosticSeverity . Info ,
33+ true ) ;
34+
35+ private static readonly DiagnosticDescriptor ModuleGenerationSuccessful = new (
36+ "INJ003" ,
37+ "Module Generation Successful" ,
38+ "Successfully generated module files for {0}." ,
39+ "Generator" ,
40+ DiagnosticSeverity . Info ,
41+ true ) ;
42+
43+ private static readonly DiagnosticDescriptor ModuleGenerationFailed = new (
44+ "INJ004" ,
45+ "Module Generation Failed" ,
46+ "An error occurred while generating module files for {0}: {1}" ,
47+ "Generator" ,
48+ DiagnosticSeverity . Error ,
49+ true ) ;
50+
51+ private static readonly Regex InvalidIdentifierCharacters = new ( "[^a-zA-Z0-9]" , RegexOptions . Compiled ) ;
52+
53+ public void Initialize ( IncrementalGeneratorInitializationContext context )
1654 {
17- private readonly record struct BuildProperties ( string ? OutputPath , string ? CsProjPath , string ? ModuleId ) ;
18-
19- private static readonly DiagnosticDescriptor MissingRequiredBuildProperties = new (
20- "INJ001" ,
21- "Missing Required Build Properties" ,
22- "OutputPath, MSBuildProjectFullPath, or ModuleId not provided. Skipping generation." ,
23- "Generator" ,
24- DiagnosticSeverity . Warning ,
25- true ) ;
26-
27- private static readonly DiagnosticDescriptor StartingModuleGeneration = new (
28- "INJ002" ,
29- "Starting Module Generation" ,
30- "Generating module files for {0}..." ,
31- "Generator" ,
32- DiagnosticSeverity . Info ,
33- true ) ;
34-
35- private static readonly DiagnosticDescriptor ModuleGenerationSuccessful = new (
36- "INJ003" ,
37- "Module Generation Successful" ,
38- "Successfully generated module files for {0}." ,
39- "Generator" ,
40- DiagnosticSeverity . Info ,
41- true ) ;
42-
43- private static readonly DiagnosticDescriptor ModuleGenerationFailed = new (
44- "INJ004" ,
45- "Module Generation Failed" ,
46- "An error occurred while generating module files for {0}: {1}" ,
47- "Generator" ,
48- DiagnosticSeverity . Error ,
49- true ) ;
50-
51- private static readonly Regex InvalidIdentifierCharacters = new ( "[^a-zA-Z0-9]" , RegexOptions . Compiled ) ;
52-
53- public void Initialize ( IncrementalGeneratorInitializationContext context )
55+ var buildProps = context . AnalyzerConfigOptionsProvider . Select ( ( provider , _ ) => new BuildProperties
5456 {
55- var buildProps = context . AnalyzerConfigOptionsProvider . Select ( ( provider , _ ) => new BuildProperties
56- {
57- OutputPath = provider . GlobalOptions . TryGetValue ( "build_property.outputpath" , out var path ) ? path : null ,
58- CsProjPath = provider . GlobalOptions . TryGetValue ( "build_property.msbuildprojectfullpath" , out var csPath ) ? csPath : null ,
59- ModuleId = provider . GlobalOptions . TryGetValue ( "build_property.moduleid" , out var moduleId ) ? moduleId :
60- provider . GlobalOptions . TryGetValue ( "build_property.modulename" , out var moduleName ) ? moduleName : null ,
61- } ) ;
62-
63- // Generate output only if all required properties are available
64- context . RegisterImplementationSourceOutput ( buildProps , ( spc , props ) =>
65- {
66- if ( props . OutputPath is null || props . CsProjPath is null || props . ModuleId is null )
67- {
68- spc . ReportDiagnostic ( Diagnostic . Create ( MissingRequiredBuildProperties , Location . None ) ) ;
69-
70- return ;
71- }
72-
73- spc . ReportDiagnostic ( Diagnostic . Create ( StartingModuleGeneration , Location . None , props . ModuleId ) ) ;
74-
75- try
76- {
77- var fullPath = Path . Combine ( Path . GetDirectoryName ( props . CsProjPath ) ! , props . OutputPath ) ;
78- GenerateAssembly ( fullPath , props . ModuleId ) ;
79- spc . ReportDiagnostic ( Diagnostic . Create ( ModuleGenerationSuccessful , Location . None , props . ModuleId ) ) ;
80- }
81- catch ( Exception ex )
82- {
83- spc . ReportDiagnostic ( Diagnostic . Create ( ModuleGenerationFailed , Location . None , props . ModuleId , $ "{ ex . GetType ( ) . Name } : { ex . Message } ") ) ;
84- }
85- } ) ;
86- }
87-
88- private static void GenerateAssembly ( string fullPath , string moduleId )
57+ OutputPath = provider . GlobalOptions . TryGetValue ( "build_property.outputpath" , out var path ) ? path : null ,
58+ CsProjPath = provider . GlobalOptions . TryGetValue ( "build_property.msbuildprojectfullpath" , out var csPath ) ? csPath : null ,
59+ ModuleId = provider . GlobalOptions . TryGetValue ( "build_property.moduleid" , out var moduleId ) ? moduleId :
60+ provider . GlobalOptions . TryGetValue ( "build_property.modulename" , out var moduleName ) ? moduleName : null ,
61+ } ) ;
62+
63+ // Generate output only if all required properties are available
64+ context . RegisterImplementationSourceOutput ( buildProps , ( spc , props ) =>
8965 {
90- CopyEmbeddedResourceToFile ( "Bannerlord.ModuleLoader.dll" , Path . Combine ( fullPath , $ "Bannerlord.ModuleLoader. { moduleId } .dll" ) , stream => SetNames ( moduleId , stream ) ) ;
91- CopyEmbeddedResourceToFile ( "Bannerlord.ModuleLoader.pdb" , Path . Combine ( fullPath , $ "Bannerlord.ModuleLoader. { moduleId } .pdb" ) ) ;
92- }
66+ if ( props . OutputPath is null || props . CsProjPath is null || props . ModuleId is null )
67+ {
68+ spc . ReportDiagnostic ( Diagnostic . Create ( MissingRequiredBuildProperties , Location . None ) ) ;
9369
94- private static void CopyEmbeddedResourceToFile ( string resourceName , string destinationPath , Func < Stream , Stream > ? transform = null )
95- {
96- using var resourceStream = typeof ( InjectorGenerator ) . Assembly . GetManifestResourceStream ( resourceName ) ;
97- if ( resourceStream is null ) return ;
70+ return ;
71+ }
9872
99- using var outputStream = new FileStream ( destinationPath , FileMode . Create , FileAccess . Write ) ;
100- using var finalStream = transform is not null ? transform ( resourceStream ) : resourceStream ;
73+ spc . ReportDiagnostic ( Diagnostic . Create ( StartingModuleGeneration , Location . None , props . ModuleId ) ) ;
10174
102- finalStream . CopyTo ( outputStream ) ;
103- }
75+ try
76+ {
77+ var fullPath = Path . Combine ( Path . GetDirectoryName ( props . CsProjPath ) ! , props . OutputPath ) ;
78+ GenerateAssembly ( fullPath , props . ModuleId ) ;
79+ spc . ReportDiagnostic ( Diagnostic . Create ( ModuleGenerationSuccessful , Location . None , props . ModuleId ) ) ;
80+ }
81+ catch ( Exception ex )
82+ {
83+ spc . ReportDiagnostic ( Diagnostic . Create ( ModuleGenerationFailed , Location . None , props . ModuleId , $ "{ ex . GetType ( ) . Name } : { ex . Message } ") ) ;
84+ }
85+ } ) ;
86+ }
10487
105- private static Stream SetNames ( string moduleName , Stream ? assemblyStream )
106- {
107- if ( assemblyStream is null )
108- throw new ArgumentNullException ( nameof ( assemblyStream ) ) ;
88+ private static void GenerateAssembly ( string fullPath , string moduleId )
89+ {
90+ CopyEmbeddedResourceToFile ( "Bannerlord.ModuleLoader.dll" , Path . Combine ( fullPath , $ "Bannerlord.ModuleLoader.{ moduleId } .dll") , stream => SetNames ( moduleId , stream ) ) ;
91+ CopyEmbeddedResourceToFile ( "Bannerlord.ModuleLoader.pdb" , Path . Combine ( fullPath , $ "Bannerlord.ModuleLoader.{ moduleId } .pdb") ) ;
92+ }
10993
110- using var modifiedAss = AssemblyDefinition . ReadAssembly ( assemblyStream ) ;
111- modifiedAss . Name . Name = moduleName ;
94+ private static void CopyEmbeddedResourceToFile ( string resourceName , string destinationPath , Func < Stream , Stream > ? transform = null )
95+ {
96+ using var resourceStream = typeof ( InjectorGenerator ) . Assembly . GetManifestResourceStream ( resourceName ) ;
97+ if ( resourceStream is null ) return ;
11298
113- var type = modifiedAss . MainModule . GetType ( "Bannerlord.ModuleLoader.SubModule" ) ;
114- type . Name = FlatName ( moduleName ) ;
99+ using var outputStream = new FileStream ( destinationPath , FileMode . Create , FileAccess . Write ) ;
100+ using var finalStream = transform is not null ? transform ( resourceStream ) : resourceStream ;
115101
116- var ms = new MemoryStream ( ) ;
117- modifiedAss . Write ( ms , new WriterParameters { DeterministicMvid = true } ) ;
118- ms . Seek ( 0 , SeekOrigin . Begin ) ;
119- return ms ;
120- }
102+ finalStream . CopyTo ( outputStream ) ;
103+ }
121104
122- private static string FlatNameRegex ( string str ) => InvalidIdentifierCharacters . Replace ( str , "_" ) ;
105+ private static Stream SetNames ( string moduleName , Stream ? assemblyStream )
106+ {
107+ if ( assemblyStream is null )
108+ throw new ArgumentNullException ( nameof ( assemblyStream ) ) ;
123109
124- private static string FlatName ( string str )
125- {
126- if ( string . IsNullOrWhiteSpace ( str ) )
127- return "_" ;
110+ using var modifiedAss = AssemblyDefinition . ReadAssembly ( assemblyStream ) ;
111+ modifiedAss . Name . Name = moduleName ;
128112
129- var sb = new StringBuilder ( str . Length ) ;
130- var firstChar = true ;
113+ var type = modifiedAss . MainModule . GetType ( "Bannerlord.ModuleLoader.SubModule" ) ;
114+ type . Name = FlatName ( moduleName ) ;
131115
132- foreach ( var c in str )
133- {
134- sb . Append ( IsValidInIdentifier ( c , firstChar ) ? c : '_' ) ;
135- firstChar = false ;
136- }
116+ var ms = new MemoryStream ( ) ;
117+ modifiedAss . Write ( ms , new WriterParameters { DeterministicMvid = true } ) ;
118+ ms . Seek ( 0 , SeekOrigin . Begin ) ;
119+ return ms ;
120+ }
137121
138- if ( char . IsDigit ( sb [ 0 ] ) )
139- sb . Insert ( 0 , '_' ) ;
122+ private static string FlatNameRegex ( string str ) => InvalidIdentifierCharacters . Replace ( str , "_" ) ;
140123
141- var result = sb . ToString ( ) ;
124+ private static string FlatName ( string str )
125+ {
126+ if ( string . IsNullOrWhiteSpace ( str ) )
127+ return "_" ;
142128
143- // Ensure it's not a C# keyword
144- if ( SyntaxFacts . IsKeywordKind ( SyntaxFacts . GetKeywordKind ( result ) ) )
145- result = $ "{ result } _";
129+ var sb = new StringBuilder ( str . Length ) ;
130+ var firstChar = true ;
146131
147- return result ;
132+ foreach ( var c in str )
133+ {
134+ sb . Append ( IsValidInIdentifier ( c , firstChar ) ? c : '_' ) ;
135+ firstChar = false ;
148136 }
149137
150- private static bool IsValidInIdentifier ( char c , bool firstChar = true ) => char . GetUnicodeCategory ( c ) switch
151- {
152- // Always allowed in C# identifiers
153- UnicodeCategory . UppercaseLetter => true ,
154- UnicodeCategory . LowercaseLetter => true ,
155- UnicodeCategory . TitlecaseLetter => true ,
156- UnicodeCategory . ModifierLetter => true ,
157- UnicodeCategory . OtherLetter => true ,
158-
159- // Only allowed after first char
160- UnicodeCategory . LetterNumber => ! firstChar ,
161- UnicodeCategory . NonSpacingMark => ! firstChar ,
162- UnicodeCategory . SpacingCombiningMark => ! firstChar ,
163- UnicodeCategory . DecimalDigitNumber => ! firstChar ,
164- UnicodeCategory . ConnectorPunctuation => ! firstChar ,
165- UnicodeCategory . Format => ! firstChar ,
166-
167- _ => false ,
168- } ;
138+ if ( char . IsDigit ( sb [ 0 ] ) )
139+ sb . Insert ( 0 , '_' ) ;
140+
141+ var result = sb . ToString ( ) ;
142+
143+ // Ensure it's not a C# keyword
144+ if ( SyntaxFacts . IsKeywordKind ( SyntaxFacts . GetKeywordKind ( result ) ) )
145+ result = $ "{ result } _";
146+
147+ return result ;
169148 }
149+
150+ private static bool IsValidInIdentifier ( char c , bool firstChar = true ) => char . GetUnicodeCategory ( c ) switch
151+ {
152+ // Always allowed in C# identifiers
153+ UnicodeCategory . UppercaseLetter => true ,
154+ UnicodeCategory . LowercaseLetter => true ,
155+ UnicodeCategory . TitlecaseLetter => true ,
156+ UnicodeCategory . ModifierLetter => true ,
157+ UnicodeCategory . OtherLetter => true ,
158+
159+ // Only allowed after the first char
160+ UnicodeCategory . LetterNumber => ! firstChar ,
161+ UnicodeCategory . NonSpacingMark => ! firstChar ,
162+ UnicodeCategory . SpacingCombiningMark => ! firstChar ,
163+ UnicodeCategory . DecimalDigitNumber => ! firstChar ,
164+ UnicodeCategory . ConnectorPunctuation => ! firstChar ,
165+ UnicodeCategory . Format => ! firstChar ,
166+
167+ _ => false ,
168+ } ;
170169}
0 commit comments