From cc822b418bb8fbc7aaeb79c92b2f91d2501670de Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:37:43 +0100 Subject: [PATCH] Initial commit of new UI c# component --- .../AppJsonSerializerContext.cs | 8 + .../DictionaryExtensions.cs | 21 ++ Il2CppInspector.Redux.GUI/Il2CppHub.cs | 47 ++++ .../Il2CppInspector.Redux.GUI.csproj | 17 ++ Il2CppInspector.Redux.GUI/LoadingSession.cs | 23 ++ .../Outputs/CSharpStubOutput.cs | 79 +++++++ .../Outputs/CppScaffoldingOutput.cs | 31 +++ .../Outputs/DisassemblerMetadataOutput.cs | 61 +++++ .../Outputs/DummyDllOutput.cs | 27 +++ .../Outputs/IOutputFormat.cs | 14 ++ .../Outputs/OutputFormatRegistry.cs | 38 +++ .../Outputs/VsSolutionOutput.cs | 29 +++ Il2CppInspector.Redux.GUI/PathHeuristics.cs | 54 +++++ Il2CppInspector.Redux.GUI/Program.cs | 54 +++++ .../Properties/launchSettings.json | 15 ++ Il2CppInspector.Redux.GUI/UiClient.cs | 44 ++++ Il2CppInspector.Redux.GUI/UiContext.cs | 216 ++++++++++++++++++ Il2CppInspector.Redux.GUI/UiProcessService.cs | 32 +++ .../appsettings.Development.json | 8 + Il2CppInspector.Redux.GUI/appsettings.json | 9 + Il2CppInspector.sln | 14 +- 21 files changed, 837 insertions(+), 4 deletions(-) create mode 100644 Il2CppInspector.Redux.GUI/AppJsonSerializerContext.cs create mode 100644 Il2CppInspector.Redux.GUI/DictionaryExtensions.cs create mode 100644 Il2CppInspector.Redux.GUI/Il2CppHub.cs create mode 100644 Il2CppInspector.Redux.GUI/Il2CppInspector.Redux.GUI.csproj create mode 100644 Il2CppInspector.Redux.GUI/LoadingSession.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/CSharpStubOutput.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/CppScaffoldingOutput.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/DisassemblerMetadataOutput.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/DummyDllOutput.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/IOutputFormat.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/OutputFormatRegistry.cs create mode 100644 Il2CppInspector.Redux.GUI/Outputs/VsSolutionOutput.cs create mode 100644 Il2CppInspector.Redux.GUI/PathHeuristics.cs create mode 100644 Il2CppInspector.Redux.GUI/Program.cs create mode 100644 Il2CppInspector.Redux.GUI/Properties/launchSettings.json create mode 100644 Il2CppInspector.Redux.GUI/UiClient.cs create mode 100644 Il2CppInspector.Redux.GUI/UiContext.cs create mode 100644 Il2CppInspector.Redux.GUI/UiProcessService.cs create mode 100644 Il2CppInspector.Redux.GUI/appsettings.Development.json create mode 100644 Il2CppInspector.Redux.GUI/appsettings.json diff --git a/Il2CppInspector.Redux.GUI/AppJsonSerializerContext.cs b/Il2CppInspector.Redux.GUI/AppJsonSerializerContext.cs new file mode 100644 index 0000000..37fd212 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/AppJsonSerializerContext.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace Il2CppInspector.Redux.GUI; + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary))] +internal partial class AppJsonSerializerContext : JsonSerializerContext; \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/DictionaryExtensions.cs b/Il2CppInspector.Redux.GUI/DictionaryExtensions.cs new file mode 100644 index 0000000..48f4476 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/DictionaryExtensions.cs @@ -0,0 +1,21 @@ +namespace Il2CppInspector.Redux.GUI; + +public static class DictionaryExtensions +{ + public static bool GetAsBooleanOrDefault(this Dictionary dict, string key, bool defaultValue) + { + if (dict.TryGetValue(key, out var value) && bool.TryParse(value, out var boolResult)) + return boolResult; + + return defaultValue; + } + + public static T GetAsEnumOrDefault(this Dictionary dict, string key, T defaultValue) + where T : struct, Enum + { + if (dict.TryGetValue(key, out var value) && Enum.TryParse(value, out var enumResult)) + return enumResult; + + return defaultValue; + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Il2CppHub.cs b/Il2CppInspector.Redux.GUI/Il2CppHub.cs new file mode 100644 index 0000000..78fe25f --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Il2CppHub.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Il2CppInspector.Redux.GUI; + +internal class Il2CppHub : Hub +{ + public UiContext State + { + get + { + if (!Context.Items.TryGetValue("context", out var context) + || context is not UiContext ctx) + { + Context.Items["context"] = ctx = new UiContext(); + } + + return ctx; + } + } + + public UiClient Client => new(Clients.Caller); + + public async Task OnUiLaunched() + { + await State.Initialize(Client); + } + + public async Task SubmitInputFiles(List inputFiles) + { + await State.LoadInputFiles(Client, inputFiles); + } + + public async Task QueueExport(string exportTypeId, string outputDirectory, Dictionary settings) + { + await State.QueueExport(Client, exportTypeId, outputDirectory, settings); + } + + public async Task StartExport() + { + await State.StartExport(Client); + } + + public async Task> GetPotentialUnityVersions() + { + return await State.GetPotentialUnityVersions(); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Il2CppInspector.Redux.GUI.csproj b/Il2CppInspector.Redux.GUI/Il2CppInspector.Redux.GUI.csproj new file mode 100644 index 0000000..b3a23ad --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Il2CppInspector.Redux.GUI.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + true + false + + + + + + + + + diff --git a/Il2CppInspector.Redux.GUI/LoadingSession.cs b/Il2CppInspector.Redux.GUI/LoadingSession.cs new file mode 100644 index 0000000..52189e1 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/LoadingSession.cs @@ -0,0 +1,23 @@ +namespace Il2CppInspector.Redux.GUI; + +public class LoadingSession : IAsyncDisposable +{ + private readonly UiClient _client; + + private LoadingSession(UiClient client) + { + _client = client; + } + + public static async Task Start(UiClient client) + { + await client.BeginLoading(); + return new LoadingSession(client); + } + + public async ValueTask DisposeAsync() + { + await _client.FinishLoading(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/CSharpStubOutput.cs b/Il2CppInspector.Redux.GUI/Outputs/CSharpStubOutput.cs new file mode 100644 index 0000000..d0dd089 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/CSharpStubOutput.cs @@ -0,0 +1,79 @@ +using Il2CppInspector.Model; +using Il2CppInspector.Outputs; + +namespace Il2CppInspector.Redux.GUI.Outputs; + +public class CSharpStubOutput : IOutputFormatProvider +{ + public static string Id => "cs"; + + private enum CSharpLayout + { + SingleFile, + Namespace, + Assembly, + Class, + Tree + } + + private enum TypeSortingMode + { + Alphabetical, + TypeDefinitionIndex + } + + private class Settings(Dictionary settings) + { + public readonly CSharpLayout Layout = settings.GetAsEnumOrDefault("layout", CSharpLayout.SingleFile); + public readonly bool FlattenHierarchy = settings.GetAsBooleanOrDefault("flatten", false); + public readonly TypeSortingMode SortingMode = settings.GetAsEnumOrDefault("sorting", TypeSortingMode.Alphabetical); + public readonly bool SuppressMetadata = settings.GetAsBooleanOrDefault("suppressmetadata", false); + public readonly bool MustCompile = settings.GetAsBooleanOrDefault("compilable", false); + public readonly bool SeperateAssemblyAttributes = settings.GetAsBooleanOrDefault("seperateassemblyattributes", true); + } + + public async Task Export(AppModel model, UiClient client, string outputPath, Dictionary settingsDict) + { + var settings = new Settings(settingsDict); + + var writer = new CSharpCodeStubs(model.TypeModel) + { + SuppressMetadata = settings.SuppressMetadata, + MustCompile = settings.MustCompile + }; + + await client.ShowLogMessage("Writing C# type definitions"); + + switch (settings.Layout, settings.SortingMode) + { + case (CSharpLayout.SingleFile, TypeSortingMode.TypeDefinitionIndex): + writer.WriteSingleFile(outputPath, info => info.Index); + break; + case (CSharpLayout.SingleFile, TypeSortingMode.Alphabetical): + writer.WriteSingleFile(outputPath, info => info.Name); + break; + + case (CSharpLayout.Namespace, TypeSortingMode.TypeDefinitionIndex): + writer.WriteFilesByNamespace(outputPath, info => info.Index, settings.FlattenHierarchy); + break; + case (CSharpLayout.Namespace, TypeSortingMode.Alphabetical): + writer.WriteFilesByNamespace(outputPath, info => info.Name, settings.FlattenHierarchy); + break; + + case (CSharpLayout.Assembly, TypeSortingMode.TypeDefinitionIndex): + writer.WriteFilesByAssembly(outputPath, info => info.Index, settings.SeperateAssemblyAttributes); + break; + case (CSharpLayout.Assembly, TypeSortingMode.Alphabetical): + writer.WriteFilesByAssembly(outputPath, info => info.Name, settings.SeperateAssemblyAttributes); + break; + + case (CSharpLayout.Class, _): + writer.WriteFilesByClass(outputPath, settings.FlattenHierarchy); + break; + + case (CSharpLayout.Tree, _): + writer.WriteFilesByClassTree(outputPath, settings.SeperateAssemblyAttributes); + break; + } + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/CppScaffoldingOutput.cs b/Il2CppInspector.Redux.GUI/Outputs/CppScaffoldingOutput.cs new file mode 100644 index 0000000..267d53b --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/CppScaffoldingOutput.cs @@ -0,0 +1,31 @@ +using Il2CppInspector.Cpp; +using Il2CppInspector.Cpp.UnityHeaders; +using Il2CppInspector.Model; +using Il2CppInspector.Outputs; + +namespace Il2CppInspector.Redux.GUI.Outputs; + +public class CppScaffoldingOutput : IOutputFormatProvider +{ + public static string Id => "cppscaffolding"; + + private class Settings(Dictionary settings) + { + public readonly string UnityVersion = settings.GetValueOrDefault("unityversion", ""); + public readonly CppCompilerType Compiler = settings.GetAsEnumOrDefault("compiler", CppCompilerType.GCC); + } + + public async Task Export(AppModel model, UiClient client, string outputPath, Dictionary settingsDict) + { + var settings = new Settings(settingsDict); + + await client.ShowLogMessage($"Bulding application model for Unity {settings.UnityVersion}/{settings.Compiler}"); + model.Build(new UnityVersion(settings.UnityVersion), settings.Compiler); + + await client.ShowLogMessage("Generating C++ scaffolding"); + var scaffolding = new CppScaffolding(model); + + await client.ShowLogMessage("Writing C++ scaffolding"); + scaffolding.Write(outputPath); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/DisassemblerMetadataOutput.cs b/Il2CppInspector.Redux.GUI/Outputs/DisassemblerMetadataOutput.cs new file mode 100644 index 0000000..3ba55f2 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/DisassemblerMetadataOutput.cs @@ -0,0 +1,61 @@ +using Il2CppInspector.Cpp; +using Il2CppInspector.Cpp.UnityHeaders; +using Il2CppInspector.Model; +using Il2CppInspector.Outputs; + +namespace Il2CppInspector.Redux.GUI.Outputs; + +public class DisassemblerMetadataOutput : IOutputFormatProvider +{ + public static string Id => "disassemblermetadata"; + + private enum DisassemblerType + { + IDA, + Ghidra, + BinaryNinja, + None + } + + private class Settings(Dictionary dict) + { + public readonly DisassemblerType Disassembler = dict.GetAsEnumOrDefault("disassembler", DisassemblerType.IDA); + public readonly string UnityVersion = dict.GetValueOrDefault("unityversion", ""); + } + + public async Task Export(AppModel model, UiClient client, string outputPath, Dictionary settingsDict) + { + var settings = new Settings(settingsDict); + + await client.ShowLogMessage($"Bulding application model for Unity {settings.UnityVersion}/{CppCompilerType.GCC}"); + model.Build(new UnityVersion(settings.UnityVersion), CppCompilerType.GCC); + + var headerPath = Path.Join(outputPath, "il2cpp.h"); + { + await client.ShowLogMessage("Generating C++ types"); + var cppScaffolding = new CppScaffolding(model, useBetterArraySize: true); + + await client.ShowLogMessage("Writing C++ types"); + cppScaffolding.WriteTypes(headerPath); + } + + var metadataPath = Path.Join(outputPath, "il2cpp.json"); + { + await client.ShowLogMessage("Generating disassembler metadata"); + var jsonMetadata = new JSONMetadata(model); + + await client.ShowLogMessage("Writing disassembler metadata"); + jsonMetadata.Write(metadataPath); + } + + if (settings.Disassembler != DisassemblerType.None) + { + var scriptPath = Path.Join(outputPath, "il2cpp.py"); + await client.ShowLogMessage($"Generating python script for {settings.Disassembler}"); + var script = new PythonScript(model); + + await client.ShowLogMessage($"Writing python script for {settings.Disassembler}"); + script.WriteScriptToFile(scriptPath, settings.Disassembler.ToString(), headerPath, metadataPath); + } + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/DummyDllOutput.cs b/Il2CppInspector.Redux.GUI/Outputs/DummyDllOutput.cs new file mode 100644 index 0000000..31bfb2f --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/DummyDllOutput.cs @@ -0,0 +1,27 @@ +using Il2CppInspector.Model; +using Il2CppInspector.Outputs; + +namespace Il2CppInspector.Redux.GUI.Outputs; + +public class DummyDllOutput : IOutputFormatProvider +{ + public static string Id => "dummydlls"; + + private class Settings(Dictionary dict) + { + public readonly bool SuppressMetadata = dict.GetAsBooleanOrDefault("suppressmetadata", false); + } + + public async Task Export(AppModel model, UiClient client, string outputPath, Dictionary settingsDict) + { + var outputSettings = new Settings(settingsDict); + + await client.ShowLogMessage("Generating .NET dummy assemblies"); + var shims = new AssemblyShims(model.TypeModel) + { + SuppressMetadata = outputSettings.SuppressMetadata + }; + + shims.Write(outputPath, client.EventHandler); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/IOutputFormat.cs b/Il2CppInspector.Redux.GUI/Outputs/IOutputFormat.cs new file mode 100644 index 0000000..86b0992 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/IOutputFormat.cs @@ -0,0 +1,14 @@ +using Il2CppInspector.Model; + +namespace Il2CppInspector.Redux.GUI.Outputs; + +public interface IOutputFormat +{ + public Task Export(AppModel model, UiClient client, string outputPath, + Dictionary settingsDict); +} + +public interface IOutputFormatProvider : IOutputFormat +{ + public static abstract string Id { get; } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/OutputFormatRegistry.cs b/Il2CppInspector.Redux.GUI/Outputs/OutputFormatRegistry.cs new file mode 100644 index 0000000..1d9f319 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/OutputFormatRegistry.cs @@ -0,0 +1,38 @@ +namespace Il2CppInspector.Redux.GUI.Outputs; + +public static class OutputFormatRegistry +{ + public static IEnumerable AvailableOutputFormats => OutputFormats.Keys; + + private static readonly Dictionary OutputFormats = []; + + public static void RegisterOutputFormat() where T : IOutputFormatProvider, new() + { + if (OutputFormats.ContainsKey(T.Id)) + throw new InvalidOperationException("An output format with this id was already registered."); + + OutputFormats[T.Id] = new T(); + } + + public static IOutputFormat GetOutputFormat(string id) + { + if (!OutputFormats.TryGetValue(id, out var format)) + throw new ArgumentException($"Failed to find output format for id {id}", nameof(id)); + + return format; + } + + private static void RegisterBuiltinOutputFormats() + { + RegisterOutputFormat(); + RegisterOutputFormat(); + RegisterOutputFormat(); + RegisterOutputFormat(); + RegisterOutputFormat(); + } + + static OutputFormatRegistry() + { + RegisterBuiltinOutputFormats(); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Outputs/VsSolutionOutput.cs b/Il2CppInspector.Redux.GUI/Outputs/VsSolutionOutput.cs new file mode 100644 index 0000000..059edab --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Outputs/VsSolutionOutput.cs @@ -0,0 +1,29 @@ +using Il2CppInspector.Model; +using Il2CppInspector.Outputs; + +namespace Il2CppInspector.Redux.GUI.Outputs; + +public class VsSolutionOutput : IOutputFormatProvider +{ + public static string Id => "vssolution"; + + private class Settings(Dictionary settings) + { + public readonly string UnityPath = settings.GetValueOrDefault("unitypath", ""); + public readonly string UnityAssembliesPath = settings.GetValueOrDefault("assembliespath", ""); + } + + public async Task Export(AppModel model, UiClient client, string outputPath, Dictionary settingsDict) + { + var settings = new Settings(settingsDict); + + var writer = new CSharpCodeStubs(model.TypeModel) + { + MustCompile = true, + SuppressMetadata = true + }; + + await client.ShowLogMessage("Writing Visual Studio solution"); + writer.WriteSolution(outputPath, settings.UnityPath, settings.UnityAssembliesPath); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/PathHeuristics.cs b/Il2CppInspector.Redux.GUI/PathHeuristics.cs new file mode 100644 index 0000000..7d52a39 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/PathHeuristics.cs @@ -0,0 +1,54 @@ +namespace Il2CppInspector.Redux.GUI; + +public static class PathHeuristics +{ + private static readonly string[] AllowedMetadataExtensionComponents = + [ + "dat", "dec" + ]; + + private static readonly string[] AllowedMetadataNameComponents = + [ + "metadata" + ]; + + private static readonly string[] AllowedBinaryPathComponents = + [ + "GameAssembly", + "il2cpp", + "UnityFramework" + ]; + + private static readonly string[] AllowedBinaryExtensionComponents = + [ + "dll", "so", "exe", "bin", "prx", "sprx", "dylib" + ]; + + public static bool IsMetadataPath(string path) + { + var extension = Path.GetExtension(path); + if (AllowedMetadataExtensionComponents.Any(extension.Contains)) + return true; + + var filename = Path.GetFileNameWithoutExtension(path); + if (AllowedMetadataNameComponents.Any(filename.Contains)) + return true; + + return false; + } + + public static bool IsBinaryPath(string path) + { + var extension = Path.GetExtension(path); + + // empty to allow macho binaries which do not have an extension + if (extension == "" || AllowedBinaryExtensionComponents.Any(extension.Contains)) + return true; + + var filename = Path.GetFileNameWithoutExtension(path); + if (AllowedBinaryPathComponents.Any(filename.Contains)) + return true; + + return false; + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Program.cs b/Il2CppInspector.Redux.GUI/Program.cs new file mode 100644 index 0000000..6d42fb0 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Program.cs @@ -0,0 +1,54 @@ +using Il2CppInspector.Redux.GUI; +using Microsoft.AspNetCore.SignalR; + +var builder = WebApplication.CreateSlimBuilder(args); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +builder.Services.AddSignalR(config => +{ +#if DEBUG + config.EnableDetailedErrors = true; +#endif +}); + +builder.Services.Configure(options => +{ + options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.SetIsOriginAllowed(origin => origin.StartsWith("http://localhost") || origin.StartsWith("http://tauri.localhost")) + .AllowAnyHeader() + .WithMethods("GET", "POST") + .AllowCredentials(); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(p => p.GetRequiredService()); + +var app = builder.Build(); + +app.UseCors(); + +app.MapHub("/il2cpp"); + +await app.StartAsync(); + +var serverUrl = app.Urls.First(); +var port = new Uri(serverUrl).Port; + +#if DEBUG +Console.WriteLine($"Listening on port {port}"); +#else +app.Services.GetRequiredService().LaunchUiProcess(port); +#endif + +await app.WaitForShutdownAsync(); \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/Properties/launchSettings.json b/Il2CppInspector.Redux.GUI/Properties/launchSettings.json new file mode 100644 index 0000000..c5dee4c --- /dev/null +++ b/Il2CppInspector.Redux.GUI/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "todos", + "applicationUrl": "http://localhost:5154", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Il2CppInspector.Redux.GUI/UiClient.cs b/Il2CppInspector.Redux.GUI/UiClient.cs new file mode 100644 index 0000000..512dd60 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/UiClient.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Il2CppInspector.Redux.GUI; + +public class UiClient(ISingleClientProxy client) +{ + private EventHandler? _handler; + + public EventHandler EventHandler + { + get + { + _handler ??= (_, status) => + { +#pragma warning disable CS4014 + ShowLogMessage(status); +#pragma warning restore CS4014 + }; + + return _handler; + } + } + + public async Task ShowLogMessage(string message, CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(ShowLogMessage), message, cancellationToken); + + public async Task BeginLoading(CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(BeginLoading), cancellationToken); + + public async Task FinishLoading(CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(FinishLoading), cancellationToken); + + public async Task ShowInfoToast(string message, CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(ShowInfoToast), message, cancellationToken); + + public async Task ShowSuccessToast(string message, CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(ShowSuccessToast), message, cancellationToken); + + public async Task ShowErrorToast(string message, CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(ShowErrorToast), message, cancellationToken); + + public async Task OnImportCompleted(CancellationToken cancellationToken = default) + => await client.SendAsync(nameof(OnImportCompleted), cancellationToken); +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/UiContext.cs b/Il2CppInspector.Redux.GUI/UiContext.cs new file mode 100644 index 0000000..c1d1ab2 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/UiContext.cs @@ -0,0 +1,216 @@ +using System.Diagnostics; +using Il2CppInspector.Cpp.UnityHeaders; +using Il2CppInspector.Model; +using Il2CppInspector.Redux.GUI.Outputs; +using Il2CppInspector.Reflection; +using Inspector = Il2CppInspector.Il2CppInspector; + +namespace Il2CppInspector.Redux.GUI; + +public class UiContext +{ + private const string BugReportSuffix = + """ + + + If you believe this is a bug in Il2CppInspectorRedux, please use the CLI version to generate the complete output and paste it when filing a bug report. + Do not send a screenshot of this error! + """; + + private Metadata? _metadata; + private IFileFormatStream? _binary; + private readonly List _appModels = []; + private readonly List _potentialUnityVersions = []; + + private readonly LoadOptions _loadOptions = new(); + + private readonly List<(string FormatId, string OutputDirectory, Dictionary Settings)> _queuedExports = []; + + private async Task TryLoadMetadataFromStream(UiClient client, MemoryStream stream) + { + try + { + _metadata = Metadata.FromStream(stream, client.EventHandler); + return true; + } + catch (Exception e) + { + await client.ShowErrorToast($"{e.Message}{BugReportSuffix}"); + } + + return false; + } + + private async Task TryLoadBinaryFromStream(UiClient client, MemoryStream stream) + { + await client.ShowLogMessage("Processing binary"); + + try + { + var file = FileFormatStream.Load(stream, _loadOptions, client.EventHandler); + + if (file == null) + throw new InvalidOperationException("Failed to determine binary file format."); + + if (file.NumImages == 0) + throw new InvalidOperationException("Failed to find any binary images in the file"); + + _binary = file; + return true; + } + catch (Exception e) + { + await client.ShowErrorToast($"{e.Message}{BugReportSuffix}"); + } + + return false; + } + + private async Task TryInitializeInspector(UiClient client) + { + Debug.Assert(_binary != null); + Debug.Assert(_metadata != null); + + _appModels.Clear(); + + var inspectors = Inspector.LoadFromStream(_binary, _metadata, client.EventHandler); + + if (inspectors.Count == 0) + { + await client.ShowErrorToast( + """ + Failed to auto-detect any IL2CPP binary images in the provided files. + This may mean the binary file is packed, encrypted or obfuscated, that the file + is not an IL2CPP image or that Il2CppInspector was not able to automatically find the required data. + Please check the binary file in a disassembler to ensure that it is an unencrypted IL2CPP binary before submitting a bug report! + """); + + return false; + } + + foreach (var inspector in inspectors) + { + await client.ShowLogMessage( + $"Building .NET type model for {inspector.BinaryImage.Format}/{inspector.BinaryImage.Arch} image"); + + try + { + var typeModel = new TypeModel(inspector); + + // Just create the app model, do not initialize it - this is done lazily depending on the exports + _appModels.Add(new AppModel(typeModel, makeDefaultBuild: false)); + } + catch (Exception e) + { + await client.ShowErrorToast($"Failed to build type model: {e.Message}{BugReportSuffix}"); + + // Clear out failed metadata and binary so subsequent loads do not use any stale data. + _metadata = null; + _binary = null; + return false; + } + } + + _potentialUnityVersions.Clear(); + _potentialUnityVersions.AddRange(UnityHeaders.GuessHeadersForBinary(_appModels[0].Package.Binary)); + + return true; + } + + public async Task Initialize(UiClient client, CancellationToken cancellationToken = default) + { + await client.ShowSuccessToast("SignalR initialized!", cancellationToken); + } + + public async Task LoadInputFiles(UiClient client, List inputFiles, + CancellationToken cancellationToken = default) + { + await using (await LoadingSession.Start(client)) + { + var streams = Inspector.GetStreamsFromPackage(inputFiles); + if (streams != null) + { + // The input files contained a package that provides the metadata and binary. + // Use these instead of parsing all files individually. + if (!await TryLoadMetadataFromStream(client, streams.Value.Metadata)) + return; + + if (!await TryLoadBinaryFromStream(client, streams.Value.Binary)) + return; + } + else + { + foreach (var inputFile in inputFiles) + { + if (_metadata != null && _binary != null) + break; + + await client.ShowLogMessage($"Processing {inputFile}", cancellationToken); + + var data = await File.ReadAllBytesAsync(inputFile, cancellationToken); + var stream = new MemoryStream(data); + + if ( _metadata == null && PathHeuristics.IsMetadataPath(inputFile)) + { + if (await TryLoadMetadataFromStream(client, stream)) + { + await client.ShowSuccessToast($"Loaded metadata from {inputFile}", cancellationToken); + } + } + else if (_binary == null && PathHeuristics.IsBinaryPath(inputFile)) + { + stream.Position = 0; + _loadOptions.BinaryFilePath = inputFile; + + if (await TryLoadBinaryFromStream(client, stream)) + { + await client.ShowSuccessToast($"Loaded binary from {inputFile}", cancellationToken); + } + } + } + } + + if (_metadata != null && _binary != null) + { + if (await TryInitializeInspector(client)) + { + await client.ShowSuccessToast("Successfully loaded IL2CPP data!", cancellationToken); + await client.OnImportCompleted(cancellationToken); + } + } + } + } + + public Task QueueExport(UiClient client, string exportFormatId, string outputDirectory, + Dictionary settings, CancellationToken cancellationToken = default) + { + _queuedExports.Add((exportFormatId, outputDirectory, settings)); + return Task.CompletedTask; + } + + public async Task StartExport(UiClient client, CancellationToken cancellationToken = default) + { + // todo: support different app model selection (when loading packages) + Debug.Assert(_appModels.Count > 0); + + await using (await LoadingSession.Start(client)) + { + var model = _appModels[0]; + + foreach (var queuedExport in _queuedExports) + { + var outputFormat = OutputFormatRegistry.GetOutputFormat(queuedExport.FormatId); + await outputFormat.Export(model, client, queuedExport.OutputDirectory, queuedExport.Settings); + } + + _queuedExports.Clear(); + } + + await client.ShowSuccessToast("Export finished", cancellationToken); + } + + public Task> GetPotentialUnityVersions() + { + return Task.FromResult(_potentialUnityVersions.Select(x => x.VersionRange.Min.ToString()).ToList()); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/UiProcessService.cs b/Il2CppInspector.Redux.GUI/UiProcessService.cs new file mode 100644 index 0000000..6719d67 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/UiProcessService.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; + +namespace Il2CppInspector.Redux.GUI; + +public class UiProcessService(IHostApplicationLifetime lifetime) : BackgroundService +{ + private const string UiExecutableName = "Il2CppInspector.Redux.GUI.UI.exe"; + + private Process? _uiProcess; + + public void LaunchUiProcess(int port) + { + _uiProcess = Process.Start(new ProcessStartInfo(UiExecutableName, [port.ToString()])); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (_uiProcess == null) + await Task.Delay(TimeSpan.FromMilliseconds(10), stoppingToken); + + await _uiProcess.WaitForExitAsync(stoppingToken); + lifetime.StopApplication(); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + if (_uiProcess is { HasExited: false }) + _uiProcess.Kill(); + + return base.StopAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.GUI/appsettings.Development.json b/Il2CppInspector.Redux.GUI/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Il2CppInspector.Redux.GUI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Il2CppInspector.Redux.GUI/appsettings.json b/Il2CppInspector.Redux.GUI/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Il2CppInspector.Redux.GUI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Il2CppInspector.sln b/Il2CppInspector.sln index 9756f3b..5ba592e 100644 --- a/Il2CppInspector.sln +++ b/Il2CppInspector.sln @@ -44,6 +44,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VersionedSerialization", "V EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VersionedSerialization.Generator", "VersionedSerialization.Generator\VersionedSerialization.Generator.csproj", "{6FF1F0C0-374A-4B7E-B173-697605679AF6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Il2CppInspector.Redux.GUI", "Il2CppInspector.Redux.GUI\Il2CppInspector.Redux.GUI.csproj", "{CB6CE40B-0805-49B1-82DD-4CAAE58F9D6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,11 +73,15 @@ Global {A24D77DA-8A64-4AD3-956A-677A96F20373}.Release|Any CPU.ActiveCfg = Release|Any CPU {A24D77DA-8A64-4AD3-956A-677A96F20373}.Release|Any CPU.Build.0 = Release|Any CPU {803C3421-1907-4114-8B6B-F5E1789FD6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {803C3421-1907-4114-8B6B-F5E1789FD6A6}.Release|Any CPU.ActiveCfg = Release|AnyCPU - {803C3421-1907-4114-8B6B-F5E1789FD6A6}.Release|Any CPU.Build.0 = Release|AnyCPU + {803C3421-1907-4114-8B6B-F5E1789FD6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {803C3421-1907-4114-8B6B-F5E1789FD6A6}.Release|Any CPU.Build.0 = Release|Any CPU {6FF1F0C0-374A-4B7E-B173-697605679AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6FF1F0C0-374A-4B7E-B173-697605679AF6}.Release|Any CPU.ActiveCfg = Release|AnyCPU - {6FF1F0C0-374A-4B7E-B173-697605679AF6}.Release|Any CPU.Build.0 = Release|AnyCPU + {6FF1F0C0-374A-4B7E-B173-697605679AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FF1F0C0-374A-4B7E-B173-697605679AF6}.Release|Any CPU.Build.0 = Release|Any CPU + {CB6CE40B-0805-49B1-82DD-4CAAE58F9D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB6CE40B-0805-49B1-82DD-4CAAE58F9D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB6CE40B-0805-49B1-82DD-4CAAE58F9D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB6CE40B-0805-49B1-82DD-4CAAE58F9D6E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE