forked from ExOK/Celeste64
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
95 changed files
with
6,908 additions
and
4,770 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<IsPackable>false</IsPackable> | ||
<Nullable>enable</Nullable> | ||
<LangVersion>latest</LangVersion> | ||
|
||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||
<IsRoslynComponent>true</IsRoslynComponent> | ||
|
||
<RootNamespace>Celeste64.HookGen</RootNamespace> | ||
<PackageId>Celeste64.HookGen</PackageId> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"> | ||
<PrivateAssets>all</PrivateAssets> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
</PackageReference> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/> | ||
|
||
<PackageReference Include="MonoMod.RuntimeDetour" Version="25.1.0-prerelease.2"/> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
||
namespace Celeste64.HookGen; | ||
|
||
[Generator] | ||
public class IncrementalHookGenerator : IIncrementalGenerator | ||
{ | ||
private const string Indent = " "; | ||
|
||
private const string GameNamespace = "Celeste64"; | ||
private const string OnHookNamespace = $"On.{GameNamespace}"; | ||
private const string ILHookNamespace = $"IL.{GameNamespace}"; | ||
|
||
private const string BindingFlags = "global::System.Reflection.BindingFlags"; | ||
private const string MethodInfo = "global::System.Reflection.MethodInfo"; | ||
private const string OnHookGenTargetAttribute = "global::Celeste64.Mod.InternalOnHookGenTargetAttribute"; | ||
private const string ILHookGenTargetAttribute = "global::Celeste64.Mod.InternalILHookGenTargetAttribute"; | ||
|
||
private const string DisallowHooksAttribute = "Celeste64.Mod.DisallowHooksAttribute"; | ||
|
||
private enum HookType | ||
{ | ||
OnHook, ILHook | ||
} | ||
|
||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
// Get all classes from the 'Celeste64' namespace | ||
var provider = context.SyntaxProvider | ||
.CreateSyntaxProvider( | ||
static (node, _) => node is ClassDeclarationSyntax, | ||
static (ctx, _) => (ClassDeclarationSyntax)ctx.Node) | ||
.Where(static type => | ||
{ | ||
if (type.Parent is not BaseNamespaceDeclarationSyntax ns) | ||
return false; | ||
if (ns.Name is not SimpleNameSyntax nsName) | ||
return false; | ||
|
||
return nsName.Identifier.Text == GameNamespace; | ||
}); | ||
|
||
context.RegisterSourceOutput(context.CompilationProvider.Combine(provider.Collect()), | ||
static (ctx, t) => | ||
{ | ||
GenerateCode(ctx, t.Left, t.Right, HookType.OnHook); | ||
GenerateCode(ctx, t.Left, t.Right, HookType.ILHook); | ||
}); | ||
} | ||
|
||
private static readonly SymbolDisplayFormat namespaceAndTypeFormat = new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); | ||
|
||
private static void GenerateCode(SourceProductionContext context, Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classDeclarations, HookType hookType) | ||
{ | ||
StringBuilder code = new(); | ||
code.AppendLine("// <auto-generated/>"); | ||
code.AppendLine(hookType switch | ||
{ | ||
HookType.OnHook => $"namespace {OnHookNamespace};", | ||
HookType.ILHook => $"namespace {ILHookNamespace};", | ||
_ => throw new ArgumentOutOfRangeException(nameof(hookType), hookType, null) | ||
}); | ||
code.AppendLine(); | ||
|
||
foreach (var classDecl in classDeclarations) | ||
{ | ||
var semanticModel = compilation.GetSemanticModel(classDecl.SyntaxTree); | ||
if (semanticModel.GetDeclaredSymbol(classDecl) is not INamedTypeSymbol classSymbol) | ||
continue; | ||
|
||
if ( | ||
// We can't hook generic types | ||
classSymbol.IsGenericType || | ||
// We don't want to hook non-public types | ||
classSymbol.DeclaredAccessibility != Accessibility.Public || | ||
// We don't want to hook disallowed types | ||
classSymbol.GetAttributes().Any(attr => attr.AttributeClass is { } attrType && attrType.ToDisplayString(namespaceAndTypeFormat) == DisallowHooksAttribute)) | ||
{ | ||
continue; | ||
} | ||
|
||
var className = classDecl.Identifier.Text; | ||
|
||
code.AppendLine($"public static class {className}"); | ||
code.AppendLine("{"); | ||
|
||
var methods = classSymbol | ||
.GetMembers() | ||
.OfType<IMethodSymbol>() | ||
.Concat(classSymbol.StaticConstructors) | ||
.ToArray(); | ||
|
||
List<(string Name, ImmutableArray<IParameterSymbol> Params)> emittedSymbols = []; | ||
foreach (var method in methods) | ||
{ | ||
if ( | ||
// We can't hook generic methods | ||
method.IsGenericMethod || | ||
// We don't want to hook non-public methods | ||
method.DeclaredAccessibility != Accessibility.Public || | ||
// We don't want to hook disallowed methods | ||
method.GetAttributes().Any(attr => attr.AttributeClass is { } attrType && attrType.ToDisplayString(namespaceAndTypeFormat) == DisallowHooksAttribute) || | ||
// There are some duplicates for, so check if this exact signature was already generated | ||
emittedSymbols.Contains((method.Name, method.Parameters))) | ||
{ | ||
continue; | ||
} | ||
emittedSymbols.Add((method.Name, method.Parameters)); | ||
|
||
var returnType = method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); | ||
var parameters = method.Parameters | ||
.Select(param => $"{param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {param.Name}") | ||
.ToArray(); | ||
var methodName = method.Name switch | ||
{ | ||
".ctor" => "ctor", | ||
".cctor" => "cctor", | ||
_ => method.Name, | ||
}; | ||
|
||
// Generate signature with parameters, if the method is overloaded | ||
// NOTE: If they have the same parameters, they are a duplicate and not an overload. | ||
var isOverloaded = methods.Any(m => m.Name == method.Name && !m.Parameters.SequenceEqual(method.Parameters)); | ||
if (isOverloaded) | ||
methodName += $"__{string.Join("__", method.Parameters.Select(param => | ||
param.Type | ||
.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) | ||
.Replace("?", "_nullable") // Use 'int_nullable' instead of 'int?' | ||
.Replace("<", "_").Replace(">", "")))}"; // Use 'List_int' instead of 'List<int>' | ||
|
||
if (hookType == HookType.OnHook) | ||
{ | ||
// orig_ delegate | ||
if (method.IsStatic) | ||
code.AppendLine($"{Indent}public delegate {returnType} orig_{methodName}({string.Join(", ", parameters)});"); | ||
else if (parameters.Length == 0) | ||
code.AppendLine( | ||
$"{Indent}public delegate {returnType} orig_{methodName}({classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} self);"); | ||
else | ||
code.AppendLine( | ||
$"{Indent}public delegate {returnType} orig_{methodName}({classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} self, {string.Join(", ", parameters)});"); | ||
|
||
// m_ MethodInfo | ||
// NOTE: Only public methods can be hooked anyway | ||
var bindingFlags = method.IsStatic | ||
? $"{BindingFlags}.Static | {BindingFlags}.Public" | ||
: $"{BindingFlags}.Instance | {BindingFlags}.Public"; | ||
var methodInfo = isOverloaded | ||
? $"typeof({classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).GetMethod(\"{method.Name}\", {bindingFlags}, [{ | ||
string.Join(", ", method.Parameters.Select(param => $"typeof({param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"))}])" | ||
: $"typeof({classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).GetMethod(\"{method.Name}\", {bindingFlags})"; | ||
code.AppendLine($"{Indent}public static readonly {MethodInfo} m_{methodName} = {methodInfo};"); | ||
} | ||
|
||
// Hook attribute | ||
code.AppendLine(hookType switch | ||
{ | ||
HookType.OnHook => $"{Indent}public sealed class {methodName}Attribute : {OnHookGenTargetAttribute}", | ||
HookType.ILHook => $"{Indent}public sealed class {methodName}Attribute : {ILHookGenTargetAttribute}", | ||
_ => throw new ArgumentOutOfRangeException(nameof(hookType), hookType, null), | ||
}); | ||
code.AppendLine($"{Indent}{{"); | ||
code.AppendLine($"{Indent}{Indent}public {methodName}Attribute()"); | ||
code.AppendLine($"{Indent}{Indent}{{"); | ||
code.AppendLine(hookType switch | ||
{ | ||
HookType.OnHook => $"{Indent}{Indent}{Indent}Target = m_{methodName};", | ||
HookType.ILHook => $"{Indent}{Indent}{Indent}Target = global::{OnHookNamespace}.{className}.m_{methodName};", // Use MethodInfo from the On. namespace | ||
_ => throw new ArgumentOutOfRangeException(nameof(hookType), hookType, null) | ||
}); | ||
code.AppendLine($"{Indent}{Indent}}}"); | ||
code.AppendLine($"{Indent}}}"); | ||
|
||
code.AppendLine(); | ||
} | ||
|
||
code.AppendLine("}"); | ||
code.AppendLine(); | ||
} | ||
|
||
context.AddSource(hookType switch | ||
{ | ||
HookType.OnHook => "OnHookGen.g.cs", | ||
HookType.ILHook => "ILHookGen.g.cs", | ||
_ => throw new ArgumentOutOfRangeException(nameof(hookType), hookType, null), | ||
}, code.ToString()); | ||
} | ||
} | ||
|
||
internal static class TypeSymbolExtensions | ||
{ | ||
/// <summary> | ||
/// Converts a generic type into a string which can be parsed by <code>Assembly.GetType</code> | ||
/// </summary> | ||
public static string ToTypeString(this ITypeSymbol typeSymbol) | ||
{ | ||
var sb = new StringBuilder(); | ||
AppendTypeString(typeSymbol, sb); | ||
return sb.ToString(); | ||
} | ||
|
||
private static readonly SymbolDisplayFormat symbolFormat = | ||
new( | ||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, | ||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, | ||
genericsOptions: SymbolDisplayGenericsOptions.None, | ||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable); | ||
|
||
private static void AppendTypeString(ITypeSymbol typeSymbol, StringBuilder sb) | ||
{ | ||
if (typeSymbol is INamedTypeSymbol namedType) | ||
{ | ||
sb.Append(namedType.ToDisplayString(symbolFormat)); | ||
if (namedType.TypeArguments.Length <= 0) return; | ||
|
||
sb.Append($"`{namedType.TypeArguments.Length}["); | ||
for (int i = 0; i < namedType.TypeArguments.Length; i++) | ||
{ | ||
if (i > 0) | ||
sb.Append(','); | ||
AppendTypeString(namedType.TypeArguments[i], sb); | ||
} | ||
|
||
sb.Append(']'); | ||
} | ||
else | ||
{ | ||
sb.Append(typeSymbol.MetadataName); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"$schema": "http://json.schemastore.org/launchsettings.json", | ||
"profiles": { | ||
"Generators": { | ||
"commandName": "DebugRoslynComponent", | ||
"targetProject": "../Celeste64/Celeste64.csproj" | ||
} | ||
} | ||
} |
4 changes: 2 additions & 2 deletions
4
Celeste64Launcher/BuildProperties.cs → Celeste64.Launcher/BuildProperties.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.