Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

- **IL2CPP support** via [Cpp2IL](https://github.com/SamboyCoding/Cpp2IL): `GameAssembly.dll` / `libil2cpp.so` + `global-metadata.dat` are detected automatically, dummy assemblies are generated and cached, and they feed the .NET explorer and MonoBehaviour field parsing.
- **.NET class explorer** — browse the game's managed assemblies as C#-like stubs (with optional IL). GUI tab **".NET Classes"**, CLI `-m dotnet`, MCP `dotnet_list` / `dotnet_type`.
- **Ghidra / Il2CppDumper package** (`-m il2cpp`) — generates `script.json`, `il2cpp.h`, `il2cpp_ghidra.h` and bundled `ghidra.py` / `ghidra_with_struct.py` scripts (patched for Ghidra Jython 2.7 **and** 11.3+ PyGhidra) so functions get named the same way [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) does. Plus `--il2cpp-lookup`, `--il2cpp-strings`, and `--il2cpp-dummy-dll` (export the dummy .NET assemblies to `<out>/DummyDll` for dnSpy / ILSpy / dotPeek).
- **Ghidra / Il2CppDumper package** (`-m il2cpp`) — generates `script.json`, `il2cpp.h`, `il2cpp_ghidra.h` and bundled `ghidra.py` / `ghidra_with_struct.py` scripts (patched for Ghidra Jython 2.7 **and** 11.3+ PyGhidra) so functions get named the same way [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) does. Plus `--il2cpp-lookup` (name ↔ address, `--il2cpp-fuzzy` for typo tolerance), `--il2cpp-strings`, `--il2cpp-decode` / `--il2cpp-data` (recover the float/double/int constants Ghidra hides as raw hex or `DAT_` loads), `--il2cpp-clean` (strip IL2CPP boilerplate from decompiled functions + annotate constants), `--il2cpp-suggest` (map a feature to the `Type$$` symbols worth decompiling), and `--il2cpp-dummy-dll` (export the dummy .NET assemblies to `<out>/DummyDll` for dnSpy / ILSpy / dotPeek).
- **Type-tree database (TPK)** — decode type-tree-stripped builds via a bundled `classdata.tpk` (`--typetree-db`, auto-loaded when present).
- **glTF 2.0 export** (`.glb` / `.gltf`) as an FBX-free alternative (meshes, skinning, materials + embedded textures, node animations).
- **Godot 4 export** (`-m godot`) — converts a game's **materials** and **particle FX** into Godot 4 scaffolds:
Expand Down Expand Up @@ -123,11 +123,18 @@ UnityRiftCLI <asset folder path> -m animator
```
UnityRiftCLI <game folder> -m il2cpp -o <output folder>
```
Look up a method/address while decompiling:
Look up a method/address while decompiling (add `--il2cpp-fuzzy` for typo-tolerant name matching):
```
UnityRiftCLI <game folder> -m il2cpp --il2cpp-lookup PlayerController$$Update
UnityRiftCLI <game folder> -m il2cpp --il2cpp-lookup 0x1A2B3C
```
Recover the constants Ghidra hides as raw hex, clean up a decompiled function, and find symbols to look at:
```
UnityRiftCLI <game folder> -m il2cpp --il2cpp-decode 0x3f19999a3e99999a # -> (0.3f, 0.6f)
UnityRiftCLI <game folder> -m il2cpp --il2cpp-data 0x4fb2ada # read the DAT_ literal from the binary
UnityRiftCLI <game folder> -m il2cpp --il2cpp-clean FUN_1800abcd.c # strip boilerplate + annotate constants
UnityRiftCLI <game folder> -m il2cpp --il2cpp-suggest parry,adrenaline # ranked Type$$ symbols to decompile
```

### Advanced Samples
- Export image assets converted to webp format to a specified output folder
Expand Down
97 changes: 97 additions & 0 deletions UnityRiftCLI/Options/CLIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ internal static class CLIOptions
public static Option<bool> f_il2cpp;
public static Option<List<string>> o_il2cppLookup;
public static Option<List<string>> o_il2cppStrings;
public static Option<List<string>> o_il2cppDecode;
public static Option<List<string>> o_il2cppData;
public static Option<List<string>> o_il2cppClean;
public static Option<List<string>> o_il2cppSuggest;
public static Option<bool> f_il2cppFuzzy;
public static Option<bool> f_il2cppCleanRaw;
public static Option<bool> f_il2cppDummyDll;
public static Option<bool> f_godotAttachPlugin;

Expand Down Expand Up @@ -681,6 +687,67 @@ private static void InitOptions()
optionExample: "Example: \"-m il2cpp --il2cpp-strings error\"\n",
optionHelpGroup: HelpGroups.Il2Cpp
);
o_il2cppDecode = new GroupedOption<List<string>>
(
optionDefaultValue: new List<string>(),
optionName: "--il2cpp-decode <hex>",
optionDescription: "Decode a packed float/double/int immediate seen in Ghidra pseudocode\n" +
"(e.g. a '= 0x3f19999a3e99999a;' store -> '(0.3f, 0.6f)'). No binary read needed.\n" +
"Only for \"-m il2cpp\". *Multiple values separated by ',' or ';' without spaces\n",
optionExample: "Example: \"-m il2cpp --il2cpp-decode 0x3f19999a3e99999a\"\n",
optionHelpGroup: HelpGroups.Il2Cpp
);
o_il2cppData = new GroupedOption<List<string>>
(
optionDefaultValue: new List<string>(),
optionName: "--il2cpp-data <va>",
optionDescription: "Read the literal-pool constant behind a DAT_<addr> load from the IL2CPP binary\n" +
"and decode it as a float/double (the hex in DAT_xxxxxxxx is the virtual address).\n" +
"Only for \"-m il2cpp\". *Multiple values separated by ',' or ';' without spaces\n",
optionExample: "Example: \"-m il2cpp --il2cpp-data 0x4fb2ada\"\n",
optionHelpGroup: HelpGroups.Il2Cpp
);
o_il2cppClean = new GroupedOption<List<string>>
(
optionDefaultValue: new List<string>(),
optionName: "--il2cpp-clean <path>",
optionDescription: "Clean Ghidra IL2CPP pseudocode: strip class-init/metadata/ctor boilerplate (by shape)\n" +
"and annotate hidden float/DAT_ constants inline. <path> is a .c file or a folder of them.\n" +
"Only for \"-m il2cpp\". *Multiple paths separated by ',' or ';' without spaces\n",
optionExample: "Example: \"-m il2cpp --il2cpp-clean out/FUN_1800abcd.c\"\n",
optionHelpGroup: HelpGroups.Il2Cpp
);
o_il2cppSuggest = new GroupedOption<List<string>>
(
optionDefaultValue: new List<string>(),
optionName: "--il2cpp-suggest <kw|file>",
optionDescription: "Suggest IL2CPP types/methods worth decompiling for the given keyword(s) or a text file\n" +
"(CamelCase / long identifiers are extracted from a file). Prints ranked Type$$ prefixes.\n" +
"Combine with --il2cpp-fuzzy for typo-tolerant matching. Only for \"-m il2cpp\".\n",
optionExample: "Example: \"-m il2cpp --il2cpp-suggest adrenaline,parry,damage\"\n",
optionHelpGroup: HelpGroups.Il2Cpp
);
f_il2cppFuzzy = new GroupedOption<bool>
(
optionDefaultValue: false,
optionName: "--il2cpp-fuzzy",
optionDescription: "(Flag) Typo-tolerant matching for --il2cpp-lookup and --il2cpp-suggest\n" +
"(ranks near-miss names by similarity instead of exact substring).\n" +
"Only for \"-m il2cpp\".\n",
optionExample: "Example: \"-m il2cpp --il2cpp-lookup PlyerController --il2cpp-fuzzy\"\n",
optionHelpGroup: HelpGroups.Il2Cpp,
isFlag: true
);
f_il2cppCleanRaw = new GroupedOption<bool>
(
optionDefaultValue: false,
optionName: "--il2cpp-clean-raw",
optionDescription: "(Flag) For --il2cpp-clean: keep the structural noise, only annotate constants.\n" +
"Only for \"-m il2cpp\".\n",
optionExample: "",
optionHelpGroup: HelpGroups.Il2Cpp,
isFlag: true
);
f_il2cppDummyDll = new GroupedOption<bool>
(
optionDefaultValue: false,
Expand Down Expand Up @@ -1030,6 +1097,18 @@ public static void ParseArgs(string[] args)
f_il2cppDummyDll.Value = true;
flagIndexes.Add(i);
break;
case "--il2cpp-fuzzy":
case "--il2cpp-clean-raw":
if (o_workMode.Value != WorkMode.Il2Cpp)
{
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{flag.Color(brightYellow)}] flag. This flag is only for \"-m il2cpp\".\n");
ShowOptionDescription(o_workMode);
return;
}
if (flag == "--il2cpp-fuzzy") f_il2cppFuzzy.Value = true;
else f_il2cppCleanRaw.Value = true;
flagIndexes.Add(i);
break;
case "--godot-attach-plugin":
if (o_workMode.Value != WorkMode.GodotScene)
{
Expand Down Expand Up @@ -1583,6 +1662,24 @@ public static void ParseArgs(string[] args)
}
o_il2cppStrings.Value.AddRange(ValueSplitter(value, isRegex: f_filterWithRegex.Value));
break;
case "--il2cpp-decode":
case "--il2cpp-data":
case "--il2cpp-clean":
case "--il2cpp-suggest":
if (o_workMode.Value != WorkMode.Il2Cpp)
{
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. This option is only for \"-m il2cpp\".\n");
ShowOptionDescription(o_workMode);
return;
}
switch (option)
{
case "--il2cpp-decode": o_il2cppDecode.Value.AddRange(ValueSplitter(value)); break;
case "--il2cpp-data": o_il2cppData.Value.AddRange(ValueSplitter(value)); break;
case "--il2cpp-clean": o_il2cppClean.Value.AddRange(ValueSplitter(value)); break;
default: o_il2cppSuggest.Value.AddRange(ValueSplitter(value)); break;
}
break;
case "--typetree-db":
if (File.Exists(value))
{
Expand Down
85 changes: 82 additions & 3 deletions UnityRiftCLI/Studio.Il2Cpp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using Ansi = UnityRift.ColorConsole;

Expand Down Expand Up @@ -86,10 +87,31 @@ public static void ExportIl2CppGhidraPackage()

var lookups = CLIOptions.o_il2cppLookup.Value;
var strings = CLIOptions.o_il2cppStrings.Value;
var decodes = CLIOptions.o_il2cppDecode.Value;
var datas = CLIOptions.o_il2cppData.Value;
var cleans = CLIOptions.o_il2cppClean.Value;
var suggests = CLIOptions.o_il2cppSuggest.Value;
var regex = CLIOptions.f_filterWithRegex.Value;
if (lookups.Count == 0 && strings.Count == 0)
var fuzzy = CLIOptions.f_il2cppFuzzy.Value;
if (lookups.Count == 0 && strings.Count == 0 && decodes.Count == 0 &&
datas.Count == 0 && cleans.Count == 0 && suggests.Count == 0)
return;

// The game binary backs DAT_ literal-pool reads (data/clean); opened lazily.
BinaryImage image = null;
var imageTried = false;
BinaryImage GetImage()
{
if (!imageTried)
{
imageTried = true;
image = BinaryImage.TryOpen(game.BinaryPath);
if (image == null)
Logger.Warning($"Could not open IL2CPP binary for literal-pool reads: {game.BinaryPath}");
}
return image;
}

var root = new JObject();
if (lookups.Count > 0)
{
Expand All @@ -104,7 +126,7 @@ public static void ExportIl2CppGhidraPackage()
}
else
{
item["result"] = idx.FindByName(q, regex);
item["result"] = idx.FindByName(q, regex, fuzzy: fuzzy);
}
arr.Add(item);
}
Expand All @@ -117,7 +139,64 @@ public static void ExportIl2CppGhidraPackage()
arr.Add(new JObject { ["query"] = q, ["result"] = idx.FindStrings(q, regex) });
root["strings"] = arr;
}
Logger.Default.Log(LoggerEvent.Info, root.ToString(Formatting.Indented), ignoreLevel: true);
if (decodes.Count > 0)
{
var arr = new JArray();
foreach (var q in decodes)
{
var item = new JObject { ["value"] = q };
if (Il2CppConstantResolver.TryParseHex(q, out var v))
item["decoded"] = Il2CppConstantResolver.DecodeImmediate(v) ?? "(not a plausible float/double/int)";
else
item["error"] = "not a hex value";
arr.Add(item);
}
root["decode"] = arr;
}
if (datas.Count > 0)
{
var arr = new JArray();
var img = GetImage();
foreach (var q in datas)
{
var item = new JObject { ["va"] = q };
if (!Il2CppConstantResolver.TryParseHex(q, out var va)) item["error"] = "not a hex address";
else if (img == null) item["error"] = "binary not available";
else item["decoded"] = img.ResolveData(va) ?? "(no plausible constant at this address)";
arr.Add(item);
}
root["data"] = arr;
}
if (cleans.Count > 0)
{
var strip = !CLIOptions.f_il2cppCleanRaw.Value;
var img = GetImage();
foreach (var pathArg in cleans)
{
var files = new List<string>();
if (Directory.Exists(pathArg)) files.AddRange(Directory.GetFiles(pathArg, "*.c"));
else if (File.Exists(pathArg)) files.Add(pathArg);
else { Logger.Warning($"--il2cpp-clean: path not found: {pathArg}"); continue; }
foreach (var f in files)
{
var cleaned = Il2CppDecompCleaner.Clean(File.ReadAllText(f), strip, floats: true, img: img);
Logger.Default.Log(LoggerEvent.Info, $"========= {Path.GetFileName(f)}\n{cleaned}", ignoreLevel: true);
}
}
}
if (suggests.Count > 0)
{
// A single arg that is a file path -> extract keywords from its text; otherwise treat as keywords.
var keywords = new List<string>();
foreach (var s in suggests)
{
if (File.Exists(s)) keywords.AddRange(Il2CppSymbolIndex.KeywordsFromText(File.ReadAllText(s)));
else keywords.Add(s);
}
root["suggest"] = idx.Suggest(keywords, methods: true, fuzzy: fuzzy);
}
if (root.HasValues)
Logger.Default.Log(LoggerEvent.Info, root.ToString(Formatting.Indented), ignoreLevel: true);
#endif
}
}
Expand Down
Loading
Loading