static void Decompile(string path, bool keepRaw = false, PsbImageFormat format = PsbImageFormat.png, uint?key = null, PsbType type = PsbType.PSB) { var name = Path.GetFileNameWithoutExtension(path); Console.WriteLine($"Decompiling: {name}"); #if !DEBUG try #endif { if (keepRaw) { PsbDecompiler.DecompileToFile(path, key: key, type: type); } else { PsbDecompiler.DecompileToFile(path, PsbExtractOption.Extract, format, key: key, type: type); } } #if !DEBUG catch (Exception e) { Console.WriteLine(e); } #endif }
static void Decompile(string path, bool extractImage = false, bool uncompress = false, bool usePng = false) { var name = Path.GetFileNameWithoutExtension(path); Console.WriteLine($"Decompiling: {name}"); #if DEBUG if (extractImage) { PsbDecompiler.DecompileToFile(path, PsbImageOption.Extract, usePng ? PsbImageFormat.Png : PsbImageFormat.Bmp); } else if (uncompress) { PsbDecompiler.DecompileToFile(path, PsbImageOption.Uncompress); } else { PsbDecompiler.DecompileToFile(path); } return; #else try { if (extractImage) { PsbDecompiler.DecompileToFile(path, PsbImageOption.Extract, usePng ? PsbImageFormat.Png : PsbImageFormat.Bmp); } else if (uncompress) { PsbDecompiler.DecompileToFile(path, PsbImageOption.Uncompress); } else { PsbDecompiler.DecompileToFile(path); } } catch (Exception e) { Console.WriteLine(e); } #endif }
// 추출 버튼 private void button1_Click(object sender, EventArgs e) { using (OpenFileDialog openFile = new OpenFileDialog()) { openFile.Title = "텍스트를 추출할 SCN 파일을 선택해주세요. (다중 선택 가능)"; openFile.DefaultExt = "scn"; openFile.Filter = "SCN 파일 (*.scn)|*.scn;"; openFile.Multiselect = true; if (openFile.ShowDialog() == DialogResult.OK) { progressBar1.Value = 0; progressBar1.Maximum = openFile.FileNames.Length; label2.Text = $"0/{progressBar1.Maximum}"; foreach (var file in openFile.FileNames) { PsbDecompiler.DecompileToFile(file); progressBar1.PerformStep(); label2.Text = $"{progressBar1.Value}/{progressBar1.Maximum}"; } progressBar1.Value = 0; foreach (var file in openFile.FileNames) { string json = file.Substring(0, file.Length - 3) + "json"; Parse(json); progressBar1.PerformStep(); label2.Text = $"{progressBar1.Value}/{progressBar1.Maximum}"; } } } }
static void ExtractArchive(string filePath, string key, Dictionary <string, object> context, bool outputRaw = true, bool extractAll = false, bool enableParallel = true) { if (File.Exists(filePath)) { var fileName = Path.GetFileName(filePath); context[Context_MdfKey] = key + fileName; var dir = Path.GetDirectoryName(Path.GetFullPath(filePath)); var name = ArchiveInfoGetPackageName(fileName); if (name == null) { Console.WriteLine($"File name incorrect: {fileName}"); name = fileName; } var body = Path.Combine(dir ?? "", name + "_body.bin"); bool hasBody = true; if (!File.Exists(body)) { Console.WriteLine($"Can not find body: {body}"); hasBody = false; } try { var baseShellType = Path.GetExtension(fileName).DefaultShellType(); PSB psb = null; using (var fs = File.OpenRead(filePath)) { psb = new PSB(MdfConvert(fs, baseShellType, context)); } File.WriteAllText(Path.GetFullPath(filePath) + ".json", PsbDecompiler.Decompile(psb)); PsbResourceJson resx = new PsbResourceJson(psb, context); var dic = psb.Objects["file_info"] as PsbDictionary; var suffixList = ((PsbList)psb.Objects["expire_suffix_list"]); var suffix = ""; if (suffixList.Count > 0) { suffix = suffixList[0] as PsbString ?? ""; } var shellType = suffix.DefaultShellType(); if (!hasBody) { //Write resx.json resx.Context[Context_ArchiveSource] = new List <string> { name }; File.WriteAllText(Path.GetFullPath(filePath) + ".resx.json", resx.SerializeToJson()); return; } Console.WriteLine($"Extracting info from {fileName} ..."); var extractDir = Path.Combine(dir, name); if (File.Exists(extractDir)) //conflict with File, not Directory { name += "-resources"; extractDir += "-resources"; } if (!Directory.Exists(extractDir)) { Directory.CreateDirectory(extractDir); } if (enableParallel) //parallel! { int count = 0; using var mmFile = MemoryMappedFile.CreateFromFile(body, FileMode.Open, name, 0, MemoryMappedFileAccess.Read); Parallel.ForEach(dic, pair => { count++; //Console.WriteLine($"{(extractAll ? "Decompiling" : "Extracting")} {pair.Key} ..."); var range = ((PsbList)pair.Value); var start = ((PsbNumber)range[0]).UIntValue; var len = ((PsbNumber)range[1]).IntValue; using var mmAccessor = mmFile.CreateViewAccessor(start, len, MemoryMappedFileAccess.Read); var bodyBytes = new byte[len]; mmAccessor.ReadArray(0, bodyBytes, 0, len); var fileNameWithSuffix = ArchiveInfoGetFileNameAppendSuffix(pair.Key, suffix); if (outputRaw) { File.WriteAllBytes(Path.Combine(extractDir, fileNameWithSuffix), bodyBytes); return; } using var ms = MsManager.GetStream(bodyBytes); var bodyContext = new Dictionary <string, object>(context) { [Context_MdfKey] = key + fileNameWithSuffix }; bodyContext.Remove(Context_ArchiveSource); using var mms = MdfConvert(ms, shellType, bodyContext); if (extractAll) { try { PSB bodyPsb = new PSB(mms); PsbDecompiler.DecompileToFile(bodyPsb, Path.Combine(extractDir, fileNameWithSuffix + ".json"), //important, must keep suffix for rebuild bodyContext, PsbExtractOption.Extract); } catch (Exception e) { Console.WriteLine($"Decompile failed: {pair.Key}"); WriteAllBytes(Path.Combine(extractDir, fileNameWithSuffix), mms); //File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } else { WriteAllBytes(Path.Combine(extractDir, fileNameWithSuffix), mms); //File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } }); Console.WriteLine($"{count} files {(extractAll ? "decompiled" : "extracted")}."); } else { //no parallel //var maxLen = dic?.Values.Max(item => item.Children(1).GetInt()) ?? 0; using var mmFile = MemoryMappedFile.CreateFromFile(body, FileMode.Open, name, 0, MemoryMappedFileAccess.Read); foreach (var pair in dic) { Console.WriteLine( $"{(extractAll ? "Decompiling" : "Extracting")} {pair.Key} ..."); var range = ((PsbList)pair.Value); var start = ((PsbNumber)range[0]).IntValue; var len = ((PsbNumber)range[1]).IntValue; using var mmAccessor = mmFile.CreateViewAccessor(start, len, MemoryMappedFileAccess.Read); var bodyBytes = new byte[len]; mmAccessor.ReadArray(0, bodyBytes, 0, len); var fileNameWithSuffix = ArchiveInfoGetFileNameAppendSuffix(pair.Key, suffix); if (outputRaw) { File.WriteAllBytes(Path.Combine(extractDir, fileNameWithSuffix), bodyBytes.AsSpan().Slice(start, len).ToArray()); continue; } using (var ms = MsManager.GetStream(bodyBytes)) { context[Context_MdfKey] = key + fileNameWithSuffix; var mms = MdfConvert(ms, shellType, context); if (extractAll) { try { PSB bodyPsb = new PSB(mms); PsbDecompiler.DecompileToFile(bodyPsb, Path.Combine(extractDir, fileNameWithSuffix + ".json"), context, PsbExtractOption.Extract); } catch (Exception e) { Console.WriteLine($"Decompile failed: {pair.Key}"); WriteAllBytes(Path.Combine(extractDir, fileNameWithSuffix), mms); //File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } else { WriteAllBytes(Path.Combine(extractDir, fileNameWithSuffix), mms); //File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } } } //Write resx.json resx.Context[Context_ArchiveSource] = new List <string> { name }; resx.Context[Context_MdfMtKey] = key; File.WriteAllText(Path.GetFullPath(filePath) + ".resx.json", resx.SerializeToJson()); } catch (Exception e) { Console.WriteLine(e); #if DEBUG throw e; #endif } } }
static void Main(string[] args) { Console.WriteLine("FreeMote PSB Decompiler"); Console.WriteLine("by Ulysses, [email protected]"); FreeMount.Init(); Console.WriteLine($"{FreeMount.PluginsCount} Plugins Loaded."); PsbConstants.InMemoryLoading = true; Console.WriteLine(); var app = new CommandLineApplication(); app.OptionsComparison = StringComparison.OrdinalIgnoreCase; //help app.HelpOption(); //do not inherit app.ExtendedHelpText = PrintHelp(); //options var optKey = app.Option <uint>("-k|--key", "Set PSB key (uint, dec)", CommandOptionType.SingleValue); var optFormat = app.Option <PsbImageFormat>("-e|--extract <FORMAT>", "Convert textures to Png/Bmp. Default=Png", CommandOptionType.SingleValue, true); var optRaw = app.Option("-raw|--raw", "Keep raw textures", CommandOptionType.NoValue, inherited: true); //メモリ足りない もうどうしよう : https://soundcloud.com/ulysses-wu/Heart-Chrome var optOom = app.Option("-oom|--memory-limit", "Disable In-Memory Loading", CommandOptionType.NoValue, inherited: true); var optHex = app.Option("-hex|--json-hex", "(Json) Use hex numbers", CommandOptionType.NoValue, true); var optArray = app.Option("-indent|--json-array-indent", "(Json) Indent arrays", CommandOptionType.NoValue, true); //args var argPath = app.Argument("Files", "File paths", multipleValues: true); //command: unlink app.Command("unlink", linkCmd => { //help linkCmd.Description = "Unlink textures from PSBs"; linkCmd.HelpOption(); linkCmd.ExtendedHelpText = @" Example: PsbDecompile unlink sample.psb "; //options var optOrder = linkCmd.Option <PsbLinkOrderBy>("-o|--order <ORDER>", "Set texture unlink order (ByName/ByOrder/Convention). Default=ByName", CommandOptionType.SingleValue); //args var argPsbPath = linkCmd.Argument("PSB", "PSB Path").IsRequired(); //var argTexPath = linkCmd.Argument("Textures", "Texture Paths").IsRequired(); linkCmd.OnExecute(() => { PsbImageFormat format = optFormat.HasValue() ? optFormat.ParsedValue : PsbImageFormat.Png; var order = optOrder.HasValue() ? optOrder.ParsedValue : PsbLinkOrderBy.Name; var psbPaths = argPsbPath.Values; foreach (var psbPath in psbPaths) { if (File.Exists(psbPath)) { try { PsbDecompiler.UnlinkToFile(psbPath, format: format, order: order); } catch (Exception e) { Console.WriteLine(e); } } } }); }); //info-psb app.Command("info-psb", archiveCmd => { //help archiveCmd.Description = "Extract files from info.psb.m & body.bin (FreeMote.Plugins required)"; archiveCmd.HelpOption(); archiveCmd.ExtendedHelpText = @" Example: PsbDecompile info-psb -k 1234567890ab -l 131 -a sample_info.psb.m PsbDecompile info-psb -s 1234567890absample_info.psb.m -l 131 sample_info.psb Hint: The body.bin should exist in the same folder and keep both file names correct. "; //options //var optMdfSeed = archiveCmd.Option("-s|--seed <SEED>", // "Set complete seed (Key+FileName)", // CommandOptionType.SingleValue); var optExtractAll = archiveCmd.Option("-a|--all", "Decompile all contents in body.bin if possible (can be slow)", CommandOptionType.NoValue); var optMdfKey = archiveCmd.Option("-k|--key <KEY>", "Set key (Infer file name from path)", CommandOptionType.SingleValue); var optMdfKeyLen = archiveCmd.Option <int>("-l|--length <LEN>", "Set key length. Default=131", CommandOptionType.SingleValue); var optInfoOom = archiveCmd.Option("-1by1|--enumerate", "Disable parallel processing when using `-a` (can be very slow)", CommandOptionType.NoValue); //args var argPsbPaths = archiveCmd.Argument("PSB", "Archive Info PSB Paths", true); archiveCmd.OnExecute(() => { bool extractAll = optExtractAll.HasValue(); bool enableParallel = PsbConstants.FastMode; if (optInfoOom.HasValue()) { enableParallel = false; } string key = optMdfKey.HasValue() ? optMdfKey.Value() : null; //string seed = optMdfSeed.HasValue() ? optMdfSeed.Value() : null; if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key), "No key or seed specified."); } int keyLen = optMdfKeyLen.HasValue() ? optMdfKeyLen.ParsedValue : 0x83; Dictionary <string, object> context = new Dictionary <string, object>(); if (keyLen >= 0) { context["MdfKeyLength"] = (uint)keyLen; } foreach (var s in argPsbPaths.Values) { if (File.Exists(s)) { var fileName = Path.GetFileName(s); context["MdfKey"] = key + fileName; try { var dir = Path.GetDirectoryName(Path.GetFullPath(s)); var name = fileName.Substring(0, fileName.IndexOf("_info.")); var body = Path.Combine(dir, name + "_body.bin"); if (!File.Exists(body)) { Console.WriteLine($"Can not find body: {body}"); continue; } PSB psb = null; using (var fs = File.OpenRead(s)) { psb = new PSB(MdfConvert(fs, context)); } File.WriteAllText(Path.GetFullPath(s) + ".json", PsbDecompiler.Decompile(psb)); PsbResourceJson resx = new PsbResourceJson(psb, context); File.WriteAllText(Path.GetFullPath(s) + ".resx.json", resx.SerializeToJson()); var dic = psb.Objects["file_info"] as PsbDictionary; var suffixList = ((PsbCollection)psb.Objects["expire_suffix_list"]); var suffix = ""; if (suffixList.Count > 0) { suffix = suffixList[0] as PsbString ?? ""; } Console.WriteLine($"Extracting info from {fileName} ..."); var bodyBytes = File.ReadAllBytes(body); var extractDir = Path.Combine(dir, name); if (!Directory.Exists(extractDir)) { Directory.CreateDirectory(extractDir); } #if DEBUG Stopwatch sw = Stopwatch.StartNew(); #endif if (enableParallel) //parallel! { int count = 0; Parallel.ForEach(dic, pair => { count++; //Console.WriteLine($"{(extractAll ? "Decompiling" : "Extracting")} {pair.Key} ..."); var range = ((PsbCollection)pair.Value); var start = ((PsbNumber)range[0]).IntValue; var len = ((PsbNumber)range[1]).IntValue; using (var ms = new MemoryStream(bodyBytes, start, len)) { var bodyContext = new Dictionary <string, object>(context) { ["MdfKey"] = key + pair.Key + suffix }; var mms = MdfConvert(ms, bodyContext); if (extractAll) { try { PSB bodyPsb = new PSB(mms); PsbDecompiler.DecompileToFile(bodyPsb, Path.Combine(extractDir, pair.Key + suffix + ".json"), bodyContext, PsbImageOption.Extract); } catch (Exception e) { Console.WriteLine($"Decompile failed: {pair.Key}"); File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } else { File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } }); Console.WriteLine($"{count} files {(extractAll ? "decompiled" : "extracted")}."); } else { //no parallel foreach (var pair in dic) { Console.WriteLine( $"{(extractAll ? "Decompiling" : "Extracting")} {pair.Key} ..."); var range = ((PsbCollection)pair.Value); var start = ((PsbNumber)range[0]).IntValue; var len = ((PsbNumber)range[1]).IntValue; using (var ms = new MemoryStream(bodyBytes, start, len)) { context["MdfKey"] = key + pair.Key + suffix; var mms = MdfConvert(ms, context); if (extractAll) { try { PSB bodyPsb = new PSB(mms); PsbDecompiler.DecompileToFile(bodyPsb, Path.Combine(extractDir, pair.Key + suffix + ".json"), context, PsbImageOption.Extract); } catch (Exception e) { Console.WriteLine($"Decompile failed: {pair.Key}"); File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } else { File.WriteAllBytes(Path.Combine(extractDir, pair.Key + suffix), mms.ToArray()); } } } } #if DEBUG sw.Stop(); Console.WriteLine($"Process time: {sw.Elapsed:g}"); #endif } catch (Exception e) { Console.WriteLine(e); } } } }); }); app.OnExecute(() => { if (optOom.HasValue()) { PsbConstants.InMemoryLoading = false; } if (optArray.HasValue()) { PsbConstants.JsonArrayCollapse = false; } if (optHex.HasValue()) { PsbConstants.JsonUseHexNumber = true; } bool useRaw = optRaw.HasValue(); PsbImageFormat format = optFormat.HasValue() ? optFormat.ParsedValue : PsbImageFormat.Png; uint?key = optKey.HasValue() ? optKey.ParsedValue : (uint?)null; foreach (var s in argPath.Values) { if (File.Exists(s)) { Decompile(s, useRaw, format, key); } else if (Directory.Exists(s)) { foreach (var file in Directory.EnumerateFiles(s, "*.psb") .Union(Directory.EnumerateFiles(s, "*.mmo")) .Union(Directory.EnumerateFiles(s, "*.pimg")) .Union(Directory.EnumerateFiles(s, "*.scn")) .Union(Directory.EnumerateFiles(s, "*.dpak")) .Union(Directory.EnumerateFiles(s, "*.psz")) .Union(Directory.EnumerateFiles(s, "*.psp")) ) { Decompile(s, useRaw, format, key); } } } }); if (args.Length == 0) { app.ShowHelp(); return; } app.Execute(args); Console.WriteLine("Done."); }