Esempio n. 1
0
        /// <summary>
        /// Pack Archive PSB
        /// </summary>
        /// <param name="jsonPath">json path</param>
        /// <param name="key">crypt key</param>
        /// <param name="intersect">Only pack files which existed in info.psb.m</param>
        /// <param name="preferPacked">Prefer using PSB files rather than json files in source folder</param>
        /// <param name="enableParallel">parallel process</param>
        /// <param name="keyLen">key length</param>
        public static void PackArchive(string jsonPath, string key, bool intersect, bool preferPacked, bool enableParallel = true,
                                       int keyLen = 131)
        {
            if (!File.Exists(jsonPath))
            {
                return;
            }
            PSB infoPsb = PsbCompiler.LoadPsbFromJsonFile(jsonPath);

            if (infoPsb.Type != PsbType.ArchiveInfo)
            {
                Console.WriteLine("Json is not an ArchiveInfo PSB.");
                return;
            }

            var resx = PsbResourceJson.LoadByPsbJsonPath(jsonPath);

            if (!resx.Context.ContainsKey(Context_ArchiveSource) ||
                resx.Context[Context_ArchiveSource] == null)
            {
                Console.WriteLine("ArchiveSource must be specified in resx.json Context.");
                return;
            }

            if (keyLen > 0)
            {
                resx.Context[Context_MdfKeyLength] = keyLen;
            }

            string infoKey = null;

            if (resx.Context[Context_MdfKey] is string mdfKey)
            {
                infoKey = mdfKey;
            }

            List <string> sourceDirs = null;

            if (resx.Context[Context_ArchiveSource] is string path)
            {
                sourceDirs = new List <string> {
                    path
                };
            }
            else if (resx.Context[Context_ArchiveSource] is IList paths)
            {
                sourceDirs = new List <string>(paths.Count);
                sourceDirs.AddRange(from object p in paths select p.ToString());
            }
            else
            {
                Console.WriteLine("ArchiveSource incorrect.");
                return;
            }

            var           baseDir = Path.GetDirectoryName(jsonPath);
            var           files   = new Dictionary <string, (string Path, ProcessMethod Method)>();
            var           suffix  = ArchiveInfoPsbGetSuffix(infoPsb);
            List <string> filter  = null;

            if (intersect)
            {
                filter = ArchiveInfoPsbCollectFiles(infoPsb, suffix);
            }

            void CollectFiles(string targetDir)
            {
                if (!Directory.Exists(targetDir))
                {
                    return;
                }

                foreach (var f in Directory.EnumerateFiles(targetDir))
                {
                    if (f.EndsWith(".resx.json", true, CultureInfo.InvariantCulture))
                    {
                        continue;
                    }
                    else if (f.EndsWith(".json", true, CultureInfo.InvariantCulture))
                    {
                        var name = Path.GetFileNameWithoutExtension(f);
                        if (preferPacked && files.ContainsKey(name) &&
                            files[name].Method != ProcessMethod.Compile)
                        {
                            //ignore
                        }
                        else
                        {
                            if (intersect && filter != null && !filter.Contains(name))
                            {
                                //ignore
                            }
                            else
                            {
                                files[name] = (f, ProcessMethod.Compile);
                            }
                        }
                    }
                    else
                    {
                        var name = Path.GetFileName(f);
                        if (!preferPacked && files.ContainsKey(name) &&
                            files[name].Method == ProcessMethod.Compile)
                        {
                            //ignore
                        }
                        else
                        {
                            if (intersect && filter != null && !filter.Contains(name))
                            {
                                //ignore
                            }
                            else
                            {
                                using var fs = File.OpenRead(f);
                                if (!MdfFile.IsSignatureMdf(fs) && name.DefaultShellType() == "MDF")
                                {
                                    files[name] = (f, ProcessMethod.EncodeMdf);
                                }
                                else
                                {
                                    files[name] = (f, ProcessMethod.None);
                                }
                            }
                        }
                    }
                }
            }

            //Collect files
            foreach (var sourceDir in sourceDirs)
            {
                CollectFiles(Path.IsPathRooted(sourceDir) ? sourceDir : Path.Combine(baseDir, sourceDir));
            }

            var fileName    = Path.GetFileName(jsonPath);
            var packageName = Path.GetFileNameWithoutExtension(fileName);

            var coreName = PsbExtension.ArchiveInfoGetPackageName(packageName);

            fileName = string.IsNullOrEmpty(coreName) ? packageName + "_body.bin" : coreName + "_body.bin";

            var fileInfoDic = new PsbDictionary(files.Count);
            var fmContext   = FreeMount.CreateContext(resx.Context);

            byte[] bodyBin = null;
            if (enableParallel)
            {
                var contents = new ConcurrentBag <(string Name, Stream Content)>();
                Parallel.ForEach(files, (kv) =>
                {
                    var fileNameWithoutSuffix = ArchiveInfoPsbGetFileName(kv.Key, suffix);
                    if (kv.Value.Method == ProcessMethod.None)
                    {
                        contents.Add((fileNameWithoutSuffix, File.OpenRead(kv.Value.Path)));
                        return;
                    }

                    var mdfContext = new Dictionary <string, object>(resx.Context);
                    var context    = FreeMount.CreateContext(mdfContext);
                    if (!string.IsNullOrEmpty(key))
                    {
                        mdfContext[Context_MdfKey] = key + fileNameWithoutSuffix + suffix;
                    }
                    else if (resx.Context[Context_MdfMtKey] is string mtKey)
                    {
                        mdfContext[Context_MdfKey] =
                            mtKey + fileNameWithoutSuffix + suffix;
                    }
                    else
                    {
                        mdfContext.Remove(Context_MdfKey);
                    }

                    mdfContext.Remove(Context_ArchiveSource);

                    if (kv.Value.Method == ProcessMethod.EncodeMdf)
                    {
                        contents.Add((fileNameWithoutSuffix, context.PackToShell(
                                          File.OpenRead(kv.Value.Path), "MDF")));
                    }
                    else
                    {
                        var content = PsbCompiler.LoadPsbAndContextFromJsonFile(kv.Value.Path);

                        var outputMdf = context.PackToShell(content.Psb.ToStream(), "MDF");
                        contents.Add((fileNameWithoutSuffix, outputMdf));
                    }
                });
Esempio n. 2
0
        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
                }
            }
        }
Esempio n. 3
0
        /// <summary>
        /// Pack Archive PSB
        /// </summary>
        /// <param name="jsonPath">json path</param>
        /// <param name="key">crypt key</param>
        /// <param name="intersect">Only pack files which existed in info.psb.m</param>
        /// <param name="preferPacked">Prefer using PSB files rather than json files in source folder</param>
        /// <param name="enableParallel">parallel process</param>
        /// <param name="keyLen">key length</param>
        /// <param name="keepRaw">Do not try to compile json or pack MDF</param>
        public static void PackArchive(string jsonPath, string key, bool intersect, bool preferPacked, bool enableParallel = true,
                                       int keyLen = 131, bool keepRaw = false)
        {
            if (!File.Exists(jsonPath))
            {
                return;
            }
            PSB infoPsb = PsbCompiler.LoadPsbFromJsonFile(jsonPath);

            if (infoPsb.Type != PsbType.ArchiveInfo)
            {
                Console.WriteLine("Json is not an ArchiveInfo PSB.");
                return;
            }

            var resx = PsbResourceJson.LoadByPsbJsonPath(jsonPath);

            if (!resx.Context.ContainsKey(Context_ArchiveSource) ||
                resx.Context[Context_ArchiveSource] == null)
            {
                Console.WriteLine("ArchiveSource must be specified in resx.json Context.");
                return;
            }

            if (keyLen > 0)
            {
                resx.Context[Context_MdfKeyLength] = keyLen;
            }

            string infoKey = null;

            if (resx.Context[Context_MdfKey] is string mdfKey)
            {
                infoKey = mdfKey;
            }

            List <string> sourceDirs = null;

            if (resx.Context[Context_ArchiveSource] is string path)
            {
                sourceDirs = new List <string> {
                    path
                };
            }
            else if (resx.Context[Context_ArchiveSource] is IList paths)
            {
                sourceDirs = new List <string>(paths.Count);
                sourceDirs.AddRange(from object p in paths select p.ToString());
            }
            else
            {
                Console.WriteLine("ArchiveSource incorrect.");
                return;
            }

            var           baseDir = Path.GetDirectoryName(jsonPath);
            var           files   = new Dictionary <string, (string Path, ProcessMethod Method)>();
            var           suffix  = ArchiveInfoGetSuffix(infoPsb);
            List <string> filter  = null;

            if (intersect) //only collect files appeared in json
            {
                filter = ArchiveInfoCollectFiles(infoPsb, suffix).ToList();
            }

            void CollectFiles(string targetDir)
            {
                if (!Directory.Exists(targetDir))
                {
                    return;
                }

                foreach (var f in Directory.EnumerateFiles(targetDir))
                {
                    if (f.EndsWith(".resx.json", true, CultureInfo.InvariantCulture))
                    {
                        continue;
                    }
                    else if (f.EndsWith(".json", true, CultureInfo.InvariantCulture)) //json source, need compile
                    {
                        var name = Path.GetFileNameWithoutExtension(f);
                        if (preferPacked && files.ContainsKey(name) &&
                            files[name].Method != ProcessMethod.Compile) //it's always right no matter set or replace
                        {
                            //ignore
                        }
                        else
                        {
                            if (intersect && filter != null && !filter.Contains(name)) //this file is not appeared in json
                            {
                                //ignore
                            }
                            else
                            {
                                files[name] = (f, keepRaw? ProcessMethod.None: ProcessMethod.Compile);
                            }
                        }
                    }
                    else
                    {
                        var name = Path.GetFileName(f);
                        if (!preferPacked && files.ContainsKey(name) &&
                            files[name].Method == ProcessMethod.Compile)
                        {
                            //ignore
                        }
                        else
                        {
                            if (intersect && filter != null && !filter.Contains(name))
                            {
                                //ignore
                            }
                            else
                            {
                                using var fs = File.OpenRead(f);
                                if (!MdfFile.IsSignatureMdf(fs) && name.DefaultShellType() == "MDF")
                                {
                                    files[name] = (f, keepRaw? ProcessMethod.None: ProcessMethod.EncodeMdf);
                                }
                                else
                                {
                                    files[name] = (f, ProcessMethod.None);
                                }
                            }
                        }
                    }
                }
            }

            //Collect files
            Console.WriteLine("Collecting files ...");
            foreach (var sourceDir in sourceDirs)
            {
                CollectFiles(Path.IsPathRooted(sourceDir) ? sourceDir : Path.Combine(baseDir, sourceDir));
            }

            Console.WriteLine($"Packing {files.Count} files ...");
            var bodyBinFileName = Path.GetFileName(jsonPath);
            var packageName     = Path.GetFileNameWithoutExtension(bodyBinFileName);

            var coreName = ArchiveInfoGetPackageName(packageName);

            bodyBinFileName = string.IsNullOrEmpty(coreName) ? packageName + "_body.bin" : coreName + "_body.bin";

            //using var mmFile =
            //    MemoryMappedFile.CreateFromFile(bodyBinFileName, FileMode.Create, coreName, );
            using var bodyFs = File.OpenWrite(bodyBinFileName);
            var fileInfoDic = new PsbDictionary(files.Count);
            var fmContext   = FreeMount.CreateContext(resx.Context);

            //byte[] bodyBin = null;
            if (enableParallel)
            {
                var contents = new ConcurrentBag <(string Name, Stream Content)>();
                Parallel.ForEach(files, (kv) =>
                {
                    var fileNameWithoutSuffix = ArchiveInfoGetFileNameRemoveSuffix(kv.Key, suffix);

                    if (kv.Value.Method == ProcessMethod.None)
                    {
                        contents.Add((fileNameWithoutSuffix, File.OpenRead(kv.Value.Path)));
                        return;
                    }

                    var mdfContext = new Dictionary <string, object>(resx.Context);
                    var context    = FreeMount.CreateContext(mdfContext);
                    if (!string.IsNullOrEmpty(key))
                    {
                        mdfContext[Context_MdfKey] = key + kv.Key;
                    }
                    else if (resx.Context[Context_MdfMtKey] is string mtKey)
                    {
                        mdfContext[Context_MdfKey] = mtKey + kv.Key;
                    }
                    else
                    {
                        mdfContext.Remove(Context_MdfKey);
                    }

                    mdfContext.Remove(Context_ArchiveSource);

                    if (kv.Value.Method == ProcessMethod.EncodeMdf)
                    {
                        using var mmFs = MemoryMappedFile.CreateFromFile(kv.Value.Path, FileMode.Open);

                        //using var fs = File.OpenRead(kv.Value.Path);
                        contents.Add((fileNameWithoutSuffix, context.PackToShell(mmFs.CreateViewStream(), "MDF"))); //disposed later
                    }
                    else
                    {
                        var content   = PsbCompiler.LoadPsbAndContextFromJsonFile(kv.Value.Path);
                        var stream    = content.Psb.ToStream();
                        var shellType = kv.Key.DefaultShellType(); //MARK: use shellType in filename, or use suffix in info?
                        if (!string.IsNullOrEmpty(shellType))
                        {
                            stream = context.PackToShell(stream, shellType); //disposed later
                        }
                        contents.Add((fileNameWithoutSuffix, stream));
                    }
                });
Esempio n. 4
0
        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.");
        }