Skip to content

Commit

Permalink
Merge branch 'dev' into beta
Browse files Browse the repository at this point in the history
  • Loading branch information
jasminegamedev committed Feb 23, 2024
2 parents d84bcb7 + befb555 commit fabf7c6
Show file tree
Hide file tree
Showing 95 changed files with 6,908 additions and 4,770 deletions.
18 changes: 15 additions & 3 deletions .github/workflows/build-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
include:
- platform: macos-latest
arch: x64
- platform: macos-latest
arch: arm64
- platform: ubuntu-latest
arch: x64
- platform: ubuntu-latest
arch: arm64
- platform: ubuntu-latest
arch: arm
- platform: windows-latest
arch: x64

runs-on: ${{ matrix.platform }}
steps:
Expand Down Expand Up @@ -47,13 +59,13 @@ jobs:
env:
os: ${{ runner.os == 'Windows' && 'win' || runner.os == 'macOS' && 'osx' || 'linux' }}
run: |
dotnet publish Celeste64Launcher/Celeste64Launcher.csproj -c Release -r ${{env.os}}-x64 -p:ImportByWildcardBeforeSolution=false "-p:EmbeddedBuildProperty=ModVersion=${{ env.SHORT_SHA }}" -o build && cp -r Content Mods build
dotnet publish Celeste64.Launcher/Celeste64.Launcher.csproj -c Release -r ${{env.os}}-x64 -p:ImportByWildcardBeforeSolution=false "-p:EmbeddedBuildProperty=ModVersion=${{ env.SHORT_SHA }}" -o build && cp -r Content Mods build
if: runner.os != 'Windows'
- name: Build (windows)
env:
os: ${{ runner.os == 'Windows' && 'win' || runner.os == 'macOS' && 'osx' || 'linux' }}
run: |
dotnet publish Celeste64Launcher/Celeste64Launcher.csproj -c Release -r win-x64 -p:ImportByWildcardBeforeSolution=false "-p:EmbeddedBuildProperty=ModVersion=${{steps.vars.outputs.SHORT_SHA}}" -o build && xcopy Mods build\Mods /s /e /h /i /y && xcopy Content build\Content /s /e /h /i /y
dotnet publish Celeste64.Launcher/Celeste64.Launcher.csproj -c Release -r win-x64 -p:ImportByWildcardBeforeSolution=false "-p:EmbeddedBuildProperty=ModVersion=${{steps.vars.outputs.SHORT_SHA}}" -o build && xcopy Mods build\Mods /s /e /h /i /y && xcopy Content build\Content /s /e /h /i /y
if: runner.os == 'Windows'
- name: Compress (Windows)
run: |
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/build-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
include:
- platform: macos-latest
arch: x64
- platform: macos-latest
arch: arm64
- platform: ubuntu-latest
arch: x64
- platform: ubuntu-latest
arch: arm64
- platform: ubuntu-latest
arch: arm
- platform: windows-latest
arch: x64

runs-on: ${{ matrix.platform }}
steps:
Expand Down Expand Up @@ -47,13 +59,13 @@ jobs:
env:
os: ${{ runner.os == 'Windows' && 'win' || runner.os == 'macOS' && 'osx' || 'linux' }}
run: |
dotnet publish Celeste64Launcher/Celeste64Launcher.csproj -c Release -r ${{env.os}}-x64 -p:ImportByWildcardBeforeSolution=false -o build && cp -r Content Mods build
dotnet publish Celeste64.Launcher/Celeste64.Launcher.csproj -c Release -r ${{env.os}}-x64 -p:ImportByWildcardBeforeSolution=false -o build && cp -r Content Mods build
if: runner.os != 'Windows'
- name: Build (windows)
env:
os: ${{ runner.os == 'Windows' && 'win' || runner.os == 'macOS' && 'osx' || 'linux' }}
run: |
dotnet publish Celeste64Launcher/Celeste64Launcher.csproj -c Release -r win-x64 -p:ImportByWildcardBeforeSolution=false -o build && xcopy Mods build\Mods /s /e /h /i /y && xcopy Content build\Content /s /e /h /i /y
dotnet publish Celeste64.Launcher/Celeste64.Launcher.csproj -c Release -r win-x64 -p:ImportByWildcardBeforeSolution=false -o build && xcopy Mods build\Mods /s /e /h /i /y && xcopy Content build\Content /s /e /h /i /y
if: runner.os == 'Windows'
- name: Compress (Windows)
run: |
Expand Down
25 changes: 25 additions & 0 deletions Celeste64.HookGen/Celeste64.HookGen.csproj
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>
237 changes: 237 additions & 0 deletions Celeste64.HookGen/IncrementalHookGenerator.cs
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);
}
}
}
9 changes: 9 additions & 0 deletions Celeste64.HookGen/Properties/launchSettings.json
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"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Celeste64;
using EmbeddedBuildProperty;

using EmbeddedBuildProperty;
namespace Celeste64.Launcher;

public partial class BuildProperties
{
Expand Down
Loading

0 comments on commit fabf7c6

Please sign in to comment.