Repository navigation
Expand file tree
/
Copy pathProgram.cs
More file actions
435 lines (395 loc) · 17.6 KB
/
Copy pathProgram.cs
File metadata and controls
435 lines (395 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
// ============================================================================
// KismetDecompiler — UE5 蓝图 Kismet 字节码 → C++ 风格伪代码
//
// 用法:
// KismetDecompiler --input <BP.uasset|BP.json|目录> --usmap <m.usmap>
// [--inline] [--opt] [--uhtdump <dir>] [--out <dir>]
// [--ext .cpp] [--jobs n]
//
// --input 单文件(.uasset / UAssetAPI 导出的 .json)或目录(递归批量)
// --inline [实验] dispatch case 体内联回事件函数
// --opt [实验] 语义优化:临时量值流内联 / 常量折叠 / 命名简化
// --uhtdump UE4SS UHTHeaderDump 目录:import 调用签名(含 out 修饰)
// --out 输出根目录(默认 ./Decompiled);镜像输入目录结构
// --ext 输出扩展名(默认 .cpp)
// --jobs 批量并发度(默认 1)
//
// 输出:每个资产一个文件(全部函数),不再是每函数一个 txt。
// ============================================================================
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using UAssetAPI;
using UAssetAPI.ExportTypes;
using UAssetAPI.Kismet.Bytecode;
using UAssetAPI.Kismet.Bytecode.Expressions;
using UAssetAPI.UnrealTypes;
using UAssetAPI.Unversioned;
using UAssetKismet;
using UAssetKismet.Experimental;
string GetArg(params string[] names)
{
foreach (var n in names)
{
var i = Array.IndexOf(args, n);
if (i >= 0 && i + 1 < args.Length) return args[i + 1];
}
return "";
}
var inputArg = GetArg("--input", "--uasset"); // 文件或目录(目录 = 批量)
var usmapPath = GetArg("--usmap");
var uhtDump = GetArg("--uhtdump");
var outRoot = GetArg("--out") is { Length: > 0 } o ? o : "Decompiled";
var extArg = GetArg("--ext");
var outExt = extArg is { Length: > 0 } ? (extArg.StartsWith('.') ? extArg : "." + extArg) : ".cpp";
var experimentalInline = args.Contains("--inline");
var optimize = args.Contains("--opt");
var jobs = int.TryParse(GetArg("--jobs"), out var jn) && jn > 0 ? jn : 1;
// --disasm [函数名子串]:转储原始字节码(开发诊断用),不做反编译
var disasmFilter = GetArg("--disasm");
var disasmMode = args.Contains("--disasm");
var flowDebug = args.Contains("--flowdebug");
var cfgDebug = args.Contains("--cfg");
if (inputArg.Length == 0 || usmapPath.Length == 0)
{
Console.WriteLine("用法: KismetDecompiler --input <蓝图.uasset|蓝图.json|目录> --usmap <mapping.usmap> [--inline] [--opt] [--uhtdump <dir>] [--out <dir>] [--ext .cpp] [--jobs n]");
return;
}
string ApplyOpt(string code)
{
if (!optimize) return code;
var lines = code.Replace("\r\n", "\n").TrimEnd('\n').Split('\n').ToList();
var text = string.Join("\n", SemanticOptimizer.OptimizeLines(lines));
// 优化会引入括号包裹与新的空块/冗余守卫,再过一遍可读性后处理。
return UAssetKismet.Experimental.ReadabilityPasses.Run(text) + "\n";
}
// ============================ 1. 收集输入资产 ============================
var assets = new List<(string Full, string Rel)>();
var skipped = new List<(string Rel, string Reason)>();
if (File.Exists(inputArg))
{
var full = Path.GetFullPath(inputArg);
assets.Add((full, Path.GetFileName(full)));
}
else if (Directory.Exists(inputArg))
{
var root = Path.GetFullPath(inputArg);
foreach (var f in EnumerateFiles(root).OrderBy(x => x, StringComparer.Ordinal))
{
var rel = Path.GetRelativePath(root, f);
var ext = Path.GetExtension(f);
if (ext.Equals(".uasset", StringComparison.OrdinalIgnoreCase)) { assets.Add((f, rel)); continue; }
if (!ext.Equals(".json", StringComparison.OrdinalIgnoreCase)) continue;
if (LooksLikeAssetJson(f)) assets.Add((f, rel));
else if (File.ReadAllText(f).Contains("\"ScriptBytecode\""))
skipped.Add((rel, "JSON 是导出数组片段(无 NameMap/Imports 包装),UAssetAPI 无法加载"));
}
}
else
{
Console.WriteLine($"[错误] 输入不存在: {inputArg}");
return;
}
if (assets.Count == 0)
{
Console.WriteLine($"[错误] 未找到可反编译的资产(*.uasset / *.json): {inputArg}");
return;
}
Directory.CreateDirectory(outRoot);
// ============================ 2. 共享上下文 ============================
var usmap = new Usmap(usmapPath);
UhtSignatureIndex? sigIndex = null;
if (uhtDump.Length > 0)
{
Console.WriteLine($"[签名] 解析 UHTHeaderDump: {uhtDump}");
var swIdx = Stopwatch.StartNew();
sigIndex = UhtSignatureIndex.Load(uhtDump);
Console.WriteLine($"[签名] 索引到 {sigIndex.ByFullKey.Count} 个 UFUNCTION({swIdx.ElapsedMilliseconds} ms)");
}
// ---- 诊断模式:直接转储字节码 / 执行流栈 / CFG,不做反编译 ----
if (disasmMode || flowDebug || cfgDebug || args.Contains("--fnsig"))
{
foreach (var (full, rel) in assets)
{
var asset = LoadAsset(full);
Console.WriteLine($"// ===== {rel} =====");
foreach (var fn in asset.Exports.OfType<FunctionExport>())
{
if (fn.ScriptBytecode is not { Length: > 0 }) continue;
var name = fn.ObjectName.ToString();
if (disasmFilter.Length > 0 && !name.Contains(disasmFilter, StringComparison.OrdinalIgnoreCase))
continue;
if (args.Contains("--fnsig")) { DumpFnSig(asset, fn); continue; }
Console.WriteLine(cfgDebug
? new StructuredKismetDecompiler(asset, name, sigIndex).DumpCfg(fn)
: flowDebug
? BytecodeDisassembler.DumpFlowConflicts(asset, fn)
: BytecodeDisassembler.DumpFunction(asset, fn, sigIndex));
}
}
return;
}
var modeText = string.Join(" + ", new[]
{
"file-level",
experimentalInline ? "inline" : null,
optimize ? "opt" : null,
sigIndex is not null ? "uht" : null
}.Where(x => x is not null));
Console.WriteLine($"[批量] {assets.Count} 个资产 | 模式: {modeText} | 输出: {Path.GetFullPath(outRoot)} | jobs={jobs}");
Console.WriteLine();
// ============================ 3. 逐个资产反编译 ============================
var sigSections = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
var failures = new ConcurrentBag<(string Rel, string Error)>();
var okCount = 0;
var totalFuncs = 0;
var totalInlined = 0;
var swAll = Stopwatch.StartNew();
void ProcessOne((string Full, string Rel) item, int ordinal)
{
var sw = Stopwatch.StartNew();
try
{
var asset = LoadAsset(item.Full);
var (text, sigText, funcCount, inlinedCount) = BuildAssetFile(asset, item.Full, item.Rel);
sigSections[item.Rel] = sigText;
var path = Path.Combine(outRoot,
item.Rel.Substring(0, item.Rel.Length - Path.GetExtension(item.Rel).Length) + outExt);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, text);
Interlocked.Increment(ref okCount);
Interlocked.Add(ref totalFuncs, funcCount);
Interlocked.Add(ref totalInlined, inlinedCount);
Console.WriteLine($"[{ordinal,3}/{assets.Count}] OK {item.Rel} 函数 {funcCount} 内联 {inlinedCount} {sw.ElapsedMilliseconds} ms");
}
catch (Exception ex)
{
failures.Add((item.Rel, ex.Message));
Console.WriteLine($"[{ordinal,3}/{assets.Count}] FAIL {item.Rel} {ex.GetType().Name}: {ex.Message}");
if (Environment.GetEnvironmentVariable("KISMETD_TRACE") == "1")
Console.WriteLine(ex.ToString());
}
}
if (jobs <= 1)
{
for (var i = 0; i < assets.Count; i++) ProcessOne(assets[i], i + 1);
}
else
{
var counter = 0;
Parallel.ForEach(assets, new ParallelOptions { MaxDegreeOfParallelism = jobs },
item => ProcessOne(item, Interlocked.Increment(ref counter)));
}
// ---- 签名清单(单文件)----
var sigPath = Path.Combine(outRoot, "_signatures.txt");
var sigAll = new StringBuilder();
foreach (var key in sigSections.Keys.OrderBy(x => x, StringComparer.Ordinal))
sigAll.AppendLine(sigSections[key]);
File.WriteAllText(sigPath, sigAll.ToString());
// ---- 汇总 ----
Console.WriteLine();
Console.WriteLine("================ 汇总 ================");
Console.WriteLine($"资产: {okCount} 成功 / {failures.Count} 失败(共 {assets.Count})");
Console.WriteLine($"函数: {totalFuncs} 个(dispatch 内联 {totalInlined} 个)");
Console.WriteLine($"输出: {Path.GetFullPath(outRoot)}");
Console.WriteLine($"签名: {Path.GetFullPath(sigPath)}");
Console.WriteLine($"用时: {swAll.Elapsed.TotalSeconds:0.0}s");
foreach (var (rel, err) in failures.OrderBy(f => f.Rel, StringComparer.Ordinal))
Console.WriteLine($" [失败] {rel}: {err}");
foreach (var (rel, reason) in skipped.OrderBy(f => f.Rel, StringComparer.Ordinal))
Console.WriteLine($" [跳过] {rel}: {reason}");
Environment.ExitCode = failures.IsEmpty ? 0 : 2;
// ============================ 辅助函数 ============================
UAsset LoadAsset(string path) =>
Path.GetExtension(path).Equals(".json", StringComparison.OrdinalIgnoreCase)
? UAsset.DeserializeJson(File.ReadAllText(path))
: new UAsset(path, EngineVersion.VER_UE5_6, usmap, CustomSerializationFlags.None);
/// <summary>反编译单个资产 → 文件级伪代码(资产头 + 函数索引 + 全部函数)。</summary>
(string Text, string SigText, int FuncCount, int InlinedCount)
BuildAssetFile(UAsset asset, string fullPath, string rel)
{
var funcs = asset.Exports.OfType<FunctionExport>()
.Where(f => f.ScriptBytecode is { Length: > 0 }).ToList();
var uber = funcs.FirstOrDefault(f => f.ObjectName.ToString().StartsWith("ExecuteUbergraph"));
var uberIdx = uber is null ? -1 : asset.Exports.IndexOf(uber) + 1;
// 收集 ExecuteUbergraph 调用点: (EntryPoint, 来源函数)
var calls = new List<(long n, string caller)>();
if (uber is not null)
{
foreach (var fn in funcs)
{
if (ReferenceEquals(fn, uber)) continue;
var n = FindUberCall(fn, uberIdx);
if (n is not null) calls.Add((n.Value, fn.ObjectName.ToString()));
}
}
var sections = new List<(string Name, string Code)>();
var inlinedCount = 0;
foreach (var fn in funcs)
{
if (ReferenceEquals(fn, uber)) continue;
var name = fn.ObjectName.ToString();
string code;
string? note = null;
if (experimentalInline && uber is not null)
{
var inlined = DispatchInliner.InlineOne(asset, uber, calls, fn, sigIndex);
if (inlined is not null) { code = inlined; note = "已内联 dispatch"; inlinedCount++; }
else code = ApplyOpt(new StructuredKismetDecompiler(asset, name, sigIndex).Decompile(fn));
}
else
{
code = ApplyOpt(new StructuredKismetDecompiler(asset, name, sigIndex).Decompile(fn));
}
sections.Add((name, SectionHeader("Function", name, note) + code));
}
// ExecuteUbergraph 反 Dispatch 放最后(inline 模式下作为参考保留完整跳转表)
if (uber is not null)
{
var dispatch = ApplyOpt(StructuredKismetDecompiler.DecompileDispatch(asset, uber, calls, sigIndex));
sections.Add((uber.ObjectName.ToString(),
SectionHeader("Function", uber.ObjectName.ToString(),
experimentalInline ? "参考: 原始反 Dispatch,case 已内联到上方各事件函数" : "反 Dispatch")
+ dispatch));
}
var sb = new StringBuilder();
sb.AppendLine("// " + new string('=', 76));
sb.AppendLine("// KismetDecompiler — 文件级伪代码");
sb.AppendLine($"// 资产 : {rel}");
sb.AppendLine($"// 源文件 : {fullPath}");
sb.AppendLine($"// usmap : {usmapPath}");
if (sigIndex is not null) sb.AppendLine($"// UHT : {uhtDump} ({sigIndex.ByFullKey.Count} 签名)");
sb.AppendLine($"// 模式 : {modeText}");
sb.AppendLine($"// 统计 : 导出 {asset.Exports.Count} 对象 / 含字节码函数 {funcs.Count} / 内联 {inlinedCount}");
sb.AppendLine($"// 生成 : {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine("// " + new string('=', 76));
sb.AppendLine();
if (sections.Count > 0)
{
sb.AppendLine("// 函数索引:");
foreach (var (name, _) in sections) sb.AppendLine($"// - {name}");
sb.AppendLine();
}
else
{
sb.AppendLine("// (该资产没有含字节码的函数)");
sb.AppendLine();
}
foreach (var (_, code) in sections) sb.AppendLine(code.TrimEnd()).AppendLine();
return (sb.ToString(), BuildSigText(asset, rel, funcs), funcs.Count, inlinedCount);
}
static string SectionHeader(string kind, string name, string? note)
{
var sb = new StringBuilder();
sb.AppendLine("// " + new string('-', 76));
sb.AppendLine($"// {kind}: {name}{(note is null ? "" : $" // {note}")}");
sb.AppendLine("// " + new string('-', 76));
return sb.ToString();
}
string BuildSigText(UAsset asset, string rel, List<FunctionExport> funcs)
{
var sb = new StringBuilder();
sb.AppendLine($"// ===== {rel} =====");
foreach (var fn in funcs)
sb.AppendLine("// " + ImportSignatures.BuildLocalSig(fn));
if (sigIndex is not null)
{
var paths = new HashSet<string>();
foreach (var fn in funcs)
if (fn.ScriptBytecode is not null)
ImportSignatures.CollectImportFuncs(asset, fn.ScriptBytecode, paths);
var hit = paths.Where(p => sigIndex.ByFullKey.ContainsKey(p)).OrderBy(x => x).ToList();
sb.AppendLine($"// -- import 调用 {paths.Count} 个,命中 UHT dump {hit.Count} 个 --");
foreach (var p in hit) sb.AppendLine($"// {sigIndex.ByFullKey[p]}");
}
sb.AppendLine();
return sb.ToString();
}
/// <summary>递归枚举非隐藏文件(跳过 .reasonix 等隐藏目录)。</summary>
static IEnumerable<string> EnumerateFiles(string root)
{
foreach (var f in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
{
var rel = Path.GetRelativePath(root, f);
if (rel.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Any(p => p.StartsWith('.')))
continue;
yield return f;
}
}
static bool LooksLikeAssetJson(string path)
{
try
{
var text = File.ReadAllText(path);
return text.Contains("\"Exports\"") && text.Contains("\"ScriptBytecode\"");
}
catch { return false; }
}
// ---- 诊断:函数签名的可用信息(参数属性表 / 局部变量读写) ----
static void DumpFnSig(UAssetAPI.UAsset asset, FunctionExport fn)
{
Console.WriteLine($"FUNCTION {fn.ObjectName} flags={fn.FunctionFlags}");
var props = fn.LoadedProperties;
Console.WriteLine($" LoadedProperties: {(props is null ? "null" : props.Length.ToString())}");
if (props is not null)
foreach (var p in props)
{
var t = p.GetType();
var fields = t.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Select(f => $"{f.Name}={f.GetValue(p)}");
Console.WriteLine($" - {p.Name} : {t.Name} flags={p.PropertyFlags} {string.Join(" ", fields)}");
}
var reads = new List<string>();
var writes = new HashSet<string>(StringComparer.Ordinal);
void Walk(KismetExpression e)
{
switch (e)
{
case EX_LocalVariable lv: reads.Add(Path(lv.Variable)); break;
}
foreach (var f in e.GetType().GetFields())
{
if (f.FieldType == typeof(KismetExpression) && f.GetValue(e) is KismetExpression sub) Walk(sub);
if (f.FieldType == typeof(KismetExpression[]) && f.GetValue(e) is KismetExpression[] arr)
foreach (var x in arr) Walk(x);
}
}
foreach (var e in fn.ScriptBytecode!) Walk(e);
var onlyRead = reads.Where(r => !r.StartsWith("out:") && !writes.Contains(r)).Distinct().ToList();
Console.WriteLine($" 先读后写(疑似参数): {string.Join(", ", onlyRead)}");
Console.WriteLine($" 被赋值(疑似局部): {string.Join(", ", writes)}");
static string Path(KismetPropertyPointer? kp) =>
kp?.New?.Path is { Length: > 0 } p ? string.Join(".", p.Select(x => x.ToString())) : "?";
}
// ---- 查找事件函数里对 ExecuteUbergraph 的调用 ----
static int? FindUberCall(FunctionExport fn, int uberExportIdx){
int? found = null;
void Walk(KismetExpression e)
{
switch (e)
{
case EX_FinalFunction f when f.StackNode.Index == uberExportIdx:
found = FirstConst(f.Parameters); return;
case EX_CallMath c when c.StackNode.Index == uberExportIdx:
found = FirstConst(c.Parameters); return;
case EX_LocalVirtualFunction l when l.VirtualFunctionName.ToString().StartsWith("ExecuteUbergraph"):
found = FirstConst(l.Parameters); return;
case EX_VirtualFunction v when v.VirtualFunctionName.ToString().StartsWith("ExecuteUbergraph"):
found = FirstConst(v.Parameters); return;
}
foreach (var f in e.GetType().GetFields())
{
if (f.FieldType == typeof(KismetExpression) && f.GetValue(e) is KismetExpression sub) Walk(sub);
if (f.FieldType == typeof(KismetExpression[]) && f.GetValue(e) is KismetExpression[] arr)
foreach (var x in arr) { if (found is not null) return; Walk(x); }
}
}
foreach (var e in fn.ScriptBytecode!) { Walk(e); if (found is not null) break; }
return found;
static int? FirstConst(KismetExpression[]? ps)
{
if (ps is null || ps.Length == 0) return null;
var raw = ps[0].GetType().GetField("RawValue")?.GetValue(ps[0]);
return raw is int i ? i : raw is byte b ? (int)b : null;
}
}