diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..17c1deef
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ build:
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '8.0.x'
+ - name: Run publish for net8.0
+ shell: pwsh
+ run: |
+ pwsh -NoProfile -ExecutionPolicy Bypass -File .\build-ci.ps1 -TargetFrameworks 'net8.0'
+
+ - name: Setup .NET 9 (optional)
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '9.0.x'
+ - name: Run publish for net9.0 (optional)
+ shell: pwsh
+ run: |
+ pwsh -NoProfile -ExecutionPolicy Bypass -File .\build-ci.ps1 -TargetFrameworks 'net9.0' -Rids 'win-x64'
diff --git a/Il2CppDumper.sln b/Il2CppDumper.sln
index 26658714..ea21670f 100644
--- a/Il2CppDumper.sln
+++ b/Il2CppDumper.sln
@@ -8,13 +8,19 @@ EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|x86.ActiveCfg = Debug|x86
+ {2087F99A-A655-41C1-84BB-54798AEA4080}.Debug|x86.Build.0 = Debug|x86
{2087F99A-A655-41C1-84BB-54798AEA4080}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2087F99A-A655-41C1-84BB-54798AEA4080}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2087F99A-A655-41C1-84BB-54798AEA4080}.Release|x86.ActiveCfg = Release|x86
+ {2087F99A-A655-41C1-84BB-54798AEA4080}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Il2CppDumper/ExecutableFormats/NSO.cs b/Il2CppDumper/ExecutableFormats/NSO.cs
index 4d964d8b..0aa58244 100644
--- a/Il2CppDumper/ExecutableFormats/NSO.cs
+++ b/Il2CppDumper/ExecutableFormats/NSO.cs
@@ -279,7 +279,7 @@ public NSO UnCompress()
var unCompressedData = new byte[header.TextSegment.DecompressedSize];
using (var decoder = new Lz4DecoderStream(new MemoryStream(textBytes)))
{
- decoder.Read(unCompressedData, 0, unCompressedData.Length);
+ ReadAll(decoder, unCompressedData);
}
writer.Write(unCompressedData);
}
@@ -293,7 +293,7 @@ public NSO UnCompress()
var unCompressedData = new byte[header.RoDataSegment.DecompressedSize];
using (var decoder = new Lz4DecoderStream(new MemoryStream(roDataBytes)))
{
- decoder.Read(unCompressedData, 0, unCompressedData.Length);
+ ReadAll(decoder, unCompressedData);
}
writer.Write(unCompressedData);
}
@@ -307,7 +307,7 @@ public NSO UnCompress()
var unCompressedData = new byte[header.DataSegment.DecompressedSize];
using (var decoder = new Lz4DecoderStream(new MemoryStream(dataBytes)))
{
- decoder.Read(unCompressedData, 0, unCompressedData.Length);
+ ReadAll(decoder, unCompressedData);
}
writer.Write(unCompressedData);
}
@@ -322,6 +322,20 @@ public NSO UnCompress()
return this;
}
+ private static void ReadAll(Stream s, byte[] buffer)
+ {
+ int offset = 0;
+ while (offset < buffer.Length)
+ {
+ int read = s.Read(buffer, offset, buffer.Length - offset);
+ if (read == 0)
+ {
+ throw new EndOfStreamException("Unexpected end of stream while reading decompressed data.");
+ }
+ offset += read;
+ }
+ }
+
public override SectionHelper GetSectionHelper(int methodCount, int typeDefinitionsCount, int imageCount)
{
var sectionHelper = new SectionHelper(this, methodCount, typeDefinitionsCount, metadataUsagesCount, imageCount);
diff --git a/Il2CppDumper/Il2CppDumper.csproj b/Il2CppDumper/Il2CppDumper.csproj
index b015a913..c52af7f7 100644
--- a/Il2CppDumper/Il2CppDumper.csproj
+++ b/Il2CppDumper/Il2CppDumper.csproj
@@ -2,13 +2,14 @@
Exe
- net6.0;net8.0
+ net9.0-windows8.0
1.0.0.0
1.0.0.0
1.0.0.0
Copyright © Perfare 2016-2024
embedded
true
+ AnyCPU;x86
diff --git a/Il2CppDumper/Program.cs b/Il2CppDumper/Program.cs
index e09ab830..0c31acad 100644
--- a/Il2CppDumper/Program.cs
+++ b/Il2CppDumper/Program.cs
@@ -3,6 +3,9 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
+using System.Collections.Generic;
+using System.Threading;
+// Add WinForms reference for dialogs
namespace Il2CppDumper
{
@@ -13,97 +16,82 @@ class Program
[STAThread]
static void Main(string[] args)
{
- config = JsonSerializer.Deserialize(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"config.json"));
+ // Ensure STAThread for WinForms dialogs
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
+ {
+ var thread = new Thread(() => Main(args));
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+ return;
+ }
+
+ config = JsonSerializer.Deserialize(
+ File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json")));
+
string il2cppPath = null;
string metadataPath = null;
string outputDir = null;
- if (args.Length == 1)
+ // ─────────────── simple flag parser ───────────────
+ var it = ((IEnumerable)args).GetEnumerator();
+ while (it.MoveNext())
{
- if (args[0] == "-h" || args[0] == "--help" || args[0] == "/?" || args[0] == "/h")
+ var arg = it.Current;
+ switch (arg)
{
- ShowHelp();
- return;
+ case "-h": case "--help": ShowHelp(); return;
+ case "-i":
+ case "--input":
+ if (!it.MoveNext()) { ShowHelp(); return; }
+ il2cppPath = it.Current; break;
+ case "-m":
+ case "--meta":
+ if (!it.MoveNext()) { ShowHelp(); return; }
+ metadataPath = it.Current; break;
+ case "-o":
+ case "--output":
+ if (!it.MoveNext()) { ShowHelp(); return; }
+ outputDir = it.Current; break;
+ default:
+ Console.WriteLine($"Unknown arg: {arg}"); ShowHelp(); return;
}
}
- if (args.Length > 3)
+ // ─────────────────────────────────────────────────
+
+ // GUI fall-backs
+ if (il2cppPath == null)
{
- ShowHelp();
- return;
+ var ofd = new OpenFileDialog { Filter = "Il2Cpp binary|*.*" };
+ if (!ofd.ShowDialog()) return;
+ il2cppPath = ofd.FileName;
}
- if (args.Length > 1)
+ if (metadataPath == null)
{
- foreach (var arg in args)
- {
- if (File.Exists(arg))
- {
- var file = File.ReadAllBytes(arg);
- if (BitConverter.ToUInt32(file, 0) == 0xFAB11BAF)
- {
- metadataPath = arg;
- }
- else
- {
- il2cppPath = arg;
- }
- }
- else if (Directory.Exists(arg))
- {
- outputDir = Path.GetFullPath(arg) + Path.DirectorySeparatorChar;
- }
- }
+ var ofd = new OpenFileDialog { Filter = "global-metadata.dat|global-metadata.dat" };
+ if (!ofd.ShowDialog()) return;
+ metadataPath = ofd.FileName;
}
- outputDir ??= AppDomain.CurrentDomain.BaseDirectory;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (outputDir == null)
{
- if (il2cppPath == null)
- {
- var ofd = new OpenFileDialog
- {
- Filter = "Il2Cpp binary file|*.*"
- };
- if (ofd.ShowDialog())
- {
- il2cppPath = ofd.FileName;
- ofd.Filter = "global-metadata|global-metadata.dat";
- if (ofd.ShowDialog())
- {
- metadataPath = ofd.FileName;
- }
- else
- {
- return;
- }
- }
- else
- {
- return;
- }
- }
+ var fbd = new FolderBrowserDialog { Description = "Select output folder" };
+ if (!fbd.ShowDialog()) return;
+ outputDir = fbd.SelectedPath + Path.DirectorySeparatorChar;
}
- if (il2cppPath == null)
+
+ // ensure absolute + trailing slash
+ outputDir = Path.GetFullPath(outputDir) + Path.DirectorySeparatorChar;
+
+ try
{
- ShowHelp();
- return;
+ if (Init(il2cppPath, metadataPath, out var metadata, out var il2Cpp))
+ Dump(metadata, il2Cpp, outputDir);
}
- if (metadataPath == null)
+ catch (Exception ex)
{
- Console.WriteLine($"ERROR: Metadata file not found or encrypted.");
- }
- else
- {
- try
- {
- if (Init(il2cppPath, metadataPath, out var metadata, out var il2Cpp))
- {
- Dump(metadata, il2Cpp, outputDir);
- }
- }
- catch (Exception e)
- {
- Console.WriteLine(e);
- }
+ Console.WriteLine(ex);
}
+
if (config.RequireAnyKey)
{
Console.WriteLine("Press any key to exit...");
@@ -113,7 +101,22 @@ static void Main(string[] args)
static void ShowHelp()
{
- Console.WriteLine($"usage: {AppDomain.CurrentDomain.FriendlyName} ");
+ Console.WriteLine("Il2CppDumper - Unity il2cpp reverse engineering tool\n");
+ Console.WriteLine("Usage:");
+ Console.WriteLine($" {AppDomain.CurrentDomain.FriendlyName} -i -m -o ");
+ Console.WriteLine();
+ Console.WriteLine("Options:");
+ Console.WriteLine(" -h, --help Show this help message and exit");
+ Console.WriteLine(" -i, --input Path to il2cpp binary file");
+ Console.WriteLine(" -m, --meta Path to global-metadata.dat file");
+ Console.WriteLine(" -o, --output Output directory");
+ Console.WriteLine();
+ Console.WriteLine("If no arguments are provided, GUI dialogs will be shown.");
+ if (config.RequireAnyKey)
+ {
+ Console.WriteLine("Press any key to exit...");
+ Console.ReadKey(true);
+ }
}
private static bool Init(string il2cppPath, string metadataPath, out Metadata metadata, out Il2Cpp il2Cpp)
diff --git a/Il2CppDumper/Resource1.Designer.cs b/Il2CppDumper/Resource1.Designer.cs
index b887e61b..9f11276f 100644
--- a/Il2CppDumper/Resource1.Designer.cs
+++ b/Il2CppDumper/Resource1.Designer.cs
@@ -1,10 +1,10 @@
//------------------------------------------------------------------------------
//
-// 此代码由工具生成。
-// 运行时版本:4.0.30319.42000
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
//
-// 对此文件的更改可能会导致不正确的行为,并且如果
-// 重新生成代码,这些更改将会丢失。
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
//
//------------------------------------------------------------------------------
@@ -13,12 +13,12 @@ namespace Il2CppDumper {
///
- /// 一个强类型的资源类,用于查找本地化的字符串等。
+ /// A strongly-typed resource class, for looking up localized strings, etc.
///
- // 此类是由 StronglyTypedResourceBuilder
- // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
- // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
- // (以 /str 作为命令选项),或重新生成 VS 项目。
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
@@ -33,7 +33,7 @@ internal Resource1() {
}
///
- /// 返回此类使用的缓存的 ResourceManager 实例。
+ /// Returns the cached ResourceManager instance used by this class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +47,8 @@ internal Resource1() {
}
///
- /// 重写当前线程的 CurrentUICulture 属性,对
- /// 使用此强类型资源类的所有资源查找执行重写。
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
@@ -61,7 +61,7 @@ internal Resource1() {
}
///
- /// 查找 System.Byte[] 类型的本地化资源。
+ /// Looks up a localized resource of type System.Byte[].
///
internal static byte[] Il2CppDummyDll {
get {
diff --git a/Il2CppDumper/Utils/OpenFileDialog.cs b/Il2CppDumper/Utils/OpenFileDialog.cs
index 80e959f2..3b8b9790 100644
--- a/Il2CppDumper/Utils/OpenFileDialog.cs
+++ b/Il2CppDumper/Utils/OpenFileDialog.cs
@@ -43,4 +43,33 @@ public bool ShowDialog()
}
}
}
+
+ public class FolderBrowserDialog
+ {
+ public string Description { get; set; }
+ public string SelectedPath { get; set; }
+
+ public bool ShowDialog()
+ {
+ var dialog = (IFileDialog)(new FileOpenDialogRCW());
+ dialog.GetOptions(out var options);
+ options |= FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM | FOS.FOS_NOVALIDATE | FOS.FOS_DONTADDTORECENT;
+ dialog.SetOptions(options);
+ if (!string.IsNullOrEmpty(Description))
+ {
+ dialog.SetTitle(Description);
+ }
+ if (dialog.Show(IntPtr.Zero) == 0)
+ {
+ dialog.GetResult(out var shellItem);
+ shellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var ppszName);
+ SelectedPath = ppszName;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/Il2CppDumper/config.json b/Il2CppDumper/config.json
index 03184fc2..74e2c6c9 100644
--- a/Il2CppDumper/config.json
+++ b/Il2CppDumper/config.json
@@ -9,7 +9,7 @@
"GenerateDummyDll": true,
"GenerateStruct": true,
"DummyDllAddToken": true,
- "RequireAnyKey": true,
+ "RequireAnyKey": false,
"ForceIl2CppVersion": false,
"ForceVersion": 16,
"ForceDump": false,
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 00000000..7d7a1178
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,15 @@
+image: Visual Studio 2022
+
+environment:
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'
+
+before_build:
+ - ps: dotnet --info
+
+build_script:
+ - cmd: powershell -NoProfile -ExecutionPolicy Bypass -File build-ci.ps1 -TargetFrameworks "net8.0"
+ - cmd: powershell -NoProfile -ExecutionPolicy Bypass -File build-ci.ps1 -TargetFrameworks "net9.0" -Rids "win-x64"
+
+artifacts:
+ - path: Il2CppDumper\\bin\\Release\\published\\**\\*
+ name: Published
diff --git a/build-ci.ps1 b/build-ci.ps1
new file mode 100644
index 00000000..147646a9
--- /dev/null
+++ b/build-ci.ps1
@@ -0,0 +1,115 @@
+Param(
+ [string[]]$TargetFrameworks = @('net8.0')
+)
+
+Set-StrictMode -Version Latest
+$project = Join-Path $PSScriptRoot 'Il2CppDumper\\Il2CppDumper.csproj'
+$configuration = 'Release'
+$outBase = Join-Path $PSScriptRoot 'Il2CppDumper\\bin\\Release'
+
+# Clean previous restore artifacts to avoid stale project.assets.json
+$projectObj = Join-Path (Split-Path $project) 'obj'
+if (Test-Path $projectObj) {
+ Write-Host "Removing stale obj folder: $projectObj"
+ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $projectObj
+}
+
+$projectBin = Join-Path (Split-Path $project) 'bin'
+if (Test-Path $projectBin) {
+ Write-Host "Removing stale bin folder: $projectBin"
+ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $projectBin
+}
+
+function Run-RestoreForTfm {
+ param($tfm)
+
+ Write-Host "Attempting dotnet restore for $tfm"
+ $restoreCmd = "dotnet restore `"$project`" --framework $tfm"
+ Write-Host "Running: $restoreCmd"
+ $restoreOutput = iex $restoreCmd 2>&1
+ if ($LASTEXITCODE -eq 0) {
+ Write-Host "dotnet restore succeeded for $tfm"
+ return
+ }
+ Write-Host "dotnet restore failed for $tfm, falling back to msbuild restore. Output:";
+ $restoreOutput | ForEach-Object { Write-Host $_ }
+ # Fallback
+ Write-Host "Restoring assets for $tfm using dotnet msbuild Restore"
+ $restoreCmd = "dotnet msbuild `"$project`" -t:Restore -p:TargetFramework=$tfm"
+ Write-Host "Running: $restoreCmd"
+ $restoreOutput = iex $restoreCmd 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host "Restore command failed. Output:";
+ $restoreOutput | ForEach-Object { Write-Host $_ }
+ throw "restore failed for $tfm"
+ }
+}
+
+function Publish-Target {
+ param($tfm, $selfContained=$false)
+
+ # Ensure per-TFM assets exist
+ Run-RestoreForTfm -tfm $tfm
+
+ $selfArg = if ($selfContained) { ' --self-contained ' } else { ' --no-self-contained ' }
+
+ $out = Join-Path $outBase "$tfm\publish"
+ Write-Host "Publishing $tfm -> $out"
+ # Framework-dependent publish (no single-file, no trimming) to avoid runtime pack/workload requirements on CI
+ $cmd = "dotnet publish `"$project`" -c $configuration -f $tfm -o `"$out`" $selfArg --no-restore"
+ Write-Host "Running: $cmd"
+ $output = iex $cmd 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host "Publish command failed. Output:";
+ $output | ForEach-Object { Write-Host $_ }
+ throw "publish failed for $tfm"
+ }
+ return $out
+}
+
+function Safe-Copy {
+ param($src, $dst)
+ if (Test-Path $src) {
+ New-Item -ItemType Directory -Force -Path (Split-Path $dst) | Out-Null
+ Copy-Item $src -Destination $dst -Force
+ Write-Host "Copied $src -> $dst"
+ } else {
+ Write-Host "Skipping missing file $src"
+ }
+}
+
+$artifacts = @()
+$publishFailures = @()
+foreach ($tfm in $TargetFrameworks) {
+ try {
+ $out = Publish-Target -tfm $tfm -selfContained:$false
+ $artifacts += $out
+ } catch {
+ $err = "Publish failed for $($tfm): $($_)"
+ Write-Host $err
+ $publishFailures += $err
+ }
+}
+
+# If any mandatory publish failed, exit with non-zero to fail CI
+if ($publishFailures.Count -gt 0) {
+ Write-Host "One or more publishes failed. Failing the CI."
+ $publishFailures | ForEach-Object { Write-Host $_ }
+ exit 1
+}
+
+# Copy or prepare artifact outputs
+$finalDir = Join-Path $outBase 'published'
+New-Item -ItemType Directory -Force -Path $finalDir | Out-Null
+foreach ($a in $artifacts | Sort-Object -Unique) {
+ if (Test-Path $a) {
+ Get-ChildItem -Path $a -Filter 'Il2CppDumper*.exe' -File -Recurse | ForEach-Object {
+ Safe-Copy $_.FullName (Join-Path $finalDir $_.Name)
+ }
+ }
+}
+
+Write-Host "Published artifacts to $finalDir"
+Write-Host "##[set-output name=artifact-path]$finalDir"
+
+exit 0