Beispiel #1
0
        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
        }
        public static void CombineImagesToFile(string inputPath, PsbImageFormat extractFormat = PsbImageFormat.png)
        {
            if (!File.Exists(inputPath))
            {
                return;
            }

            var psb = new PSB(inputPath);

            if (psb.Type != PsbType.Tachie)
            {
                return;
            }

            var name    = Path.GetFileNameWithoutExtension(inputPath);
            var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name);

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                Directory.CreateDirectory(dirPath);
            }

            var bitmaps = CombineTachie(psb);

            foreach (var kv in bitmaps)
            {
                kv.Value.Save(Path.Combine(dirPath, $"{kv.Key}.{extractFormat}"), extractFormat.ToImageFormat());
            }
        }
Beispiel #3
0
        public static ImageFormat ToImageFormat(this PsbImageFormat imageFormat)
        {
            switch (imageFormat)
            {
            case PsbImageFormat.bmp:
                return(ImageFormat.Bmp);

            case PsbImageFormat.png:
            default:
                return(ImageFormat.Png);
            }
        }
Beispiel #4
0
        /// <summary>
        /// Decompile to files
        /// </summary>
        /// <param name="psb">PSB</param>
        /// <param name="outputPath">Output json file name, should end with .json</param>
        /// <param name="additionalContext">additional context used in decompilation</param>
        /// <param name="extractOption">whether to extract image to common format</param>
        /// <param name="extractFormat">if extract, what format do you want</param>
        /// <param name="useResx">if false, use array-based resource json (legacy)</param>
        /// <param name="key">PSB CryptKey</param>
        public static void DecompileToFile(PSB psb, string outputPath, Dictionary <string, object> additionalContext = null, PsbExtractOption extractOption = PsbExtractOption.Original,
                                           PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true, uint?key = null)
        {
            var context = FreeMount.CreateContext(additionalContext);

            if (key != null)
            {
                context.Context[Consts.Context_CryptKey] = key;
            }

            File.WriteAllText(outputPath, Decompile(psb)); //MARK: breaking change for json path

            OutputResources(psb, context, outputPath, extractOption, extractFormat, useResx);
        }
Beispiel #5
0
        public static string DefaultExtension(this PsbImageFormat imageFormat)
        {
            switch (imageFormat)
            {
            case PsbImageFormat.png:
                return(".png");

            case PsbImageFormat.bmp:
                return(".bmp");

            default:
                return($".{imageFormat}");
            }
        }
Beispiel #6
0
        /// <summary>
        /// Decompile to files
        /// </summary>
        /// <param name="inputPath">PSB file path</param>
        /// <param name="extractOption">whether to extract image to common format</param>
        /// <param name="extractFormat">if extract, what format do you want</param>
        /// <param name="useResx">if false, use array-based resource json (legacy)</param>
        /// <param name="key">PSB CryptKey</param>
        /// <param name="type">Specify PSB type, if not set, infer type automatically</param>
        public static void DecompileToFile(string inputPath, PsbExtractOption extractOption = PsbExtractOption.Original,
                                           PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true, uint?key = null, PsbType type = PsbType.PSB)
        {
            var context = FreeMount.CreateContext();

            if (key != null)
            {
                context.Context[Consts.Context_CryptKey] = key;
            }

            File.WriteAllText(ChangeExtensionForOutputJson(inputPath, ".json"), Decompile(inputPath, out var psb, context.Context));

            OutputResources(psb, context, inputPath, extractOption, extractFormat, useResx);
        }
Beispiel #7
0
        public static void DecompressToImageFile(byte[] data, string path, int height, int width,
                                                 PsbImageFormat format = PsbImageFormat.png, PsbPixelFormat colorFormat = PsbPixelFormat.None, int align = 4)
        {
            byte[] bytes;
            try
            {
                bytes = Decompress(data, height, width, align);
            }
            catch (Exception e)
            {
                throw new PsbBadFormatException(PsbBadFormatReason.Resources, "data incorrect", e);
            }

            ConvertToImageFile(bytes, path, height, width, format, colorFormat);
        }
Beispiel #8
0
        /// <summary>
        /// Decompile to files
        /// </summary>
        /// <param name="inputPath">PSB file path</param>
        /// <param name="imageOption">whether to extract image to common format</param>
        /// <param name="extractFormat">if extract, what format do you want</param>
        /// <param name="useResx">if false, use array-based resource json (legacy)</param>
        /// <param name="key">PSB CryptKey</param>
        public static void DecompileToFile(string inputPath, PsbImageOption imageOption = PsbImageOption.Original,
                                           PsbImageFormat extractFormat = PsbImageFormat.Png, bool useResx = true, uint?key = null)
        {
            var context = FreeMount.CreateContext();

            if (key != null)
            {
                context.Context[FreeMount.CryptKey] = key;
            }

            File.WriteAllText(Path.ChangeExtension(inputPath, ".json"),
                              Decompile(inputPath, out var psb, context.Context)); //MARK: breaking change for json path

            OutputResources(psb, context, inputPath, imageOption, extractFormat, useResx);
        }
Beispiel #9
0
        /// <summary>
        /// Save (most user friendly) images
        /// </summary>
        /// <param name="inputPath"></param>
        /// <param name="format"></param>
        public static void ExtractImageFiles(string inputPath, PsbImageFormat format = PsbImageFormat.png)
        {
            if (!File.Exists(inputPath))
            {
                return;
            }

            var name    = Path.GetFileNameWithoutExtension(inputPath);
            var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name);

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                Directory.CreateDirectory(dirPath);
            }

            var texExt    = format == PsbImageFormat.bmp ? ".bmp" : ".png";
            var texFormat = format.ToImageFormat();

            var psb = new PSB(inputPath);

            if (psb.Type == PsbType.Tachie)
            {
                var bitmaps = TextureCombiner.CombineTachie(psb);
                foreach (var kv in bitmaps)
                {
                    kv.Value.Save(Path.Combine(dirPath, $"{kv.Key}{texExt}"), texFormat);
                }
                return;
            }

            var texs = PsbResHelper.UnlinkImages(psb);

            foreach (var tex in texs)
            {
                tex.Save(Path.Combine(dirPath, tex.Tag + texExt), texFormat);
            }
        }
Beispiel #10
0
        internal static void OutputResources(PSB psb, FreeMountContext context, string filePath, PsbExtractOption extractOption = PsbExtractOption.Original,
                                             PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true)
        {
            var             name    = Path.GetFileNameWithoutExtension(filePath);
            var             dirPath = Path.Combine(Path.GetDirectoryName(filePath), name);
            PsbResourceJson resx    = new PsbResourceJson(psb, context.Context);

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                if (psb.Resources.Count != 0)
                {
                    Directory.CreateDirectory(dirPath);
                }
            }

            var resDictionary = psb.TypeHandler.OutputResources(psb, context, name, dirPath, extractOption);

            //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json`
            if (useResx)
            {
                resx.Resources = resDictionary;
                resx.Context   = context.Context;
                File.WriteAllText(Path.ChangeExtension(filePath, ".resx.json"),
                                  JsonConvert.SerializeObject(resx, Formatting.Indented));
            }
            else
            {
                File.WriteAllText(Path.ChangeExtension(filePath, ".res.json"),
                                  JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented));
            }
        }
Beispiel #11
0
        internal static void OutputResources(PSB psb, FreeMountContext context, string filePath, PsbImageOption imageOption = PsbImageOption.Original,
                                             PsbImageFormat extractFormat = PsbImageFormat.Png, bool useResx = true)
        {
            var name    = Path.GetFileNameWithoutExtension(filePath);
            var dirPath = Path.Combine(Path.GetDirectoryName(filePath), name);

            var             resources = psb.CollectResources();
            PsbResourceJson resx      = new PsbResourceJson(psb, context.Context);

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                if (psb.Resources.Count != 0 || resources.Count != 0)
                {
                    Directory.CreateDirectory(dirPath);
                }
            }

            Dictionary <string, string> resDictionary = new Dictionary <string, string>();

            if (imageOption == PsbImageOption.Original)
            {
                for (int i = 0; i < psb.Resources.Count; i++)
                {
                    var relativePath = psb.Resources[i].Index == null ? $"#{i}.bin" : $"{psb.Resources[i].Index}.bin";

                    File.WriteAllBytes(
                        Path.Combine(dirPath, relativePath),
                        psb.Resources[i].Data);
                    resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}");
                }
            }
            else
            {
                for (int i = 0; i < resources.Count; i++)
                {
                    var resource = resources[i];
                    //Generate Friendly Name
                    string relativePath = resource.GetFriendlyName(psb.Type);

                    switch (imageOption)
                    {
                    case PsbImageOption.Extract:
                        ImageFormat pixelFormat;
                        switch (extractFormat)
                        {
                        case PsbImageFormat.Png:
                            relativePath += ".png";
                            pixelFormat   = ImageFormat.Png;
                            break;

                        default:
                            relativePath += ".bmp";
                            pixelFormat   = ImageFormat.Bmp;
                            break;
                        }

                        relativePath = CheckPath(relativePath, i);
                        if (resource.Compress == PsbCompressType.RL)
                        {
                            RL.DecompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                     resource.Height, resource.Width, extractFormat, resource.PixelFormat);
                        }
                        else if (resource.Compress == PsbCompressType.Tlg ||
                                 resource.Compress == PsbCompressType.ByName)
                        {
                            var bmp = context.ResourceToBitmap(resource.Compress == PsbCompressType.Tlg
                                    ? ".tlg"
                                    : Path.GetExtension(resource.Name), resource.Data);
                            if (bmp == null)
                            {
                                if (resource.Compress == PsbCompressType.Tlg)     //Fallback to managed TLG decoder
                                {
                                    using (var ms = new MemoryStream(resource.Data))
                                        using (var br = new BinaryReader(ms))
                                        {
                                            bmp = new TlgImageConverter().Read(br);
                                            bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat);
                                            bmp.Dispose();
                                        }
                                }

                                relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name));
                                File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                            }
                            else
                            {
                                bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat);
                                bmp.Dispose();
                            }
                        }
                        //else if (resource.Compress == PsbCompressType.ByName)
                        //{
                        //    relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name));
                        //    File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        //}
                        else
                        {
                            RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                  resource.Height, resource.Width, extractFormat, resource.PixelFormat);
                        }

                        break;

                    case PsbImageOption.Original:
                        if (resources[i].Compress == PsbCompressType.RL)
                        {
                            relativePath += ".rl";
                            relativePath  = CheckPath(relativePath, i);
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        else if (resource.Compress == PsbCompressType.Tlg)
                        {
                            relativePath += ".tlg";
                            relativePath  = CheckPath(relativePath, i);
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        else
                        {
                            relativePath += ".raw";
                            relativePath  = CheckPath(relativePath, i);
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }

                        break;

                    case PsbImageOption.Decompress:
                        relativePath += ".raw";
                        relativePath  = CheckPath(relativePath, i);
                        File.WriteAllBytes(Path.Combine(dirPath, relativePath),
                                           resources[i].Compress == PsbCompressType.RL
                                    ? RL.Decompress(resource.Data)
                                    : resource.Data);
                        break;

                    case PsbImageOption.Compress:
                        relativePath += ".rl";
                        relativePath  = CheckPath(relativePath, i);
                        File.WriteAllBytes(Path.Combine(dirPath, relativePath),
                                           resources[i].Compress != PsbCompressType.RL
                                    ? RL.Compress(resource.Data)
                                    : resource.Data);
                        break;

                    default:
                        throw new ArgumentOutOfRangeException(nameof(imageOption), imageOption, null);
                    }

                    try
                    {
                        resDictionary.Add(i.ToString(), $"{name}/{relativePath}");
                    }
                    catch (ArgumentException e)
                    {
                        throw new PsbBadFormatException(PsbBadFormatReason.Resources,
                                                        "There are resources with same names! Try Raw export mode.", e);
                    }
                }
            }

            //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json`
            if (useResx)
            {
                resx.Resources = resDictionary;
                resx.Context   = context.Context;
                File.WriteAllText(Path.ChangeExtension(filePath, ".resx.json"),
                                  JsonConvert.SerializeObject(resx, Formatting.Indented));
            }
            else
            {
                File.WriteAllText(Path.ChangeExtension(filePath, ".res.json"),
                                  JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented));
            }

            string CheckPath(string rPath, int id)
            {
                var k = Path.GetFileNameWithoutExtension(rPath);

                if (resDictionary.ContainsKey(k))
                {
                    return($"{id}{Path.GetExtension(rPath)}");
                }

                return(rPath);
            }
        }
Beispiel #12
0
        /// <summary>
        /// Convert a PSB to External Texture PSB.
        /// </summary>
        /// <param name="inputPath"></param>
        /// <param name="outputUnlinkedPsb">output unlinked PSB; otherwise only output textures</param>
        /// <param name="order"></param>
        /// <param name="format"></param>
        /// <returns>The unlinked PSB path</returns>
        public static string UnlinkToFile(string inputPath, bool outputUnlinkedPsb = true, PsbLinkOrderBy order = PsbLinkOrderBy.Name, PsbImageFormat format = PsbImageFormat.png)
        {
            if (!File.Exists(inputPath))
            {
                return(null);
            }

            var name        = Path.GetFileNameWithoutExtension(inputPath);
            var dirPath     = Path.Combine(Path.GetDirectoryName(inputPath), name);
            var psbSavePath = inputPath;

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                Directory.CreateDirectory(dirPath);
            }

            var context = FreeMount.CreateContext();

            context.ImageFormat = format;
            var psb = new PSB(inputPath);

            if (psb.TypeHandler is BaseImageType imageType)
            {
                imageType.UnlinkToFile(psb, context, name, dirPath, outputUnlinkedPsb, order);
            }

            psb.TypeHandler.UnlinkToFile(psb, context, name, dirPath, outputUnlinkedPsb, order);

            if (outputUnlinkedPsb)
            {
                psb.Merge();
                psbSavePath = Path.ChangeExtension(inputPath, ".unlinked.psb"); //unlink only works with motion.psb so no need for ext rename
                File.WriteAllBytes(psbSavePath, psb.Build());
            }

            return(psbSavePath);
        }
Beispiel #13
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);
            //メモリ足りない もうどうしよう : https://soundcloud.com/ulysses-wu/Heart-Chrome
            var optOom = app.Option("-oom|--memory-limit", "Disable In-Memory Loading", CommandOptionType.NoValue);

            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);
                            }
                            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.");
        }
Beispiel #14
0
        /// <summary>
        /// Decompile to files
        /// </summary>
        /// <param name="inputPath">PSB file path</param>
        /// <param name="imageOption">whether to extract image to common format</param>
        /// <param name="extractFormat">if extract, what format do you want</param>
        /// <param name="useResx">if false, use array-based resource json (legacy)</param>
        public static void DecompileToFile(string inputPath, PsbImageOption imageOption = PsbImageOption.Original, PsbImageFormat extractFormat = PsbImageFormat.Png, bool useResx = true)
        {
            var name    = Path.GetFileNameWithoutExtension(inputPath);
            var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name);

            File.WriteAllText(inputPath + ".json", Decompile(inputPath, out var psb));
            var             resources = psb.CollectResources();
            PsbResourceJson resx      = new PsbResourceJson
            {
                PsbVersion       = psb.Header.Version,
                PsbType          = psb.Type,
                Platform         = psb.Platform,
                ExternalTextures = psb.Type == PsbType.Motion && psb.Resources.Count <= 0
            };

            if (!Directory.Exists(dirPath)) //ensure no file with same name!
            {
                Directory.CreateDirectory(dirPath);
            }
            Dictionary <string, string> resDictionary = new Dictionary <string, string>();

            if (imageOption == PsbImageOption.Original)
            {
                for (int i = 0; i < psb.Resources.Count; i++)
                {
                    var relativePath = psb.Resources[i].Index == null ? $"#{i}.bin" : $"{psb.Resources[i].Index}.bin";

                    File.WriteAllBytes(
                        Path.Combine(dirPath, relativePath),
                        psb.Resources[i].Data);
                    resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}");
                }
            }
            else
            {
                for (int i = 0; i < resources.Count; i++)
                {
                    var resource = resources[i];
                    //Generate Friendly Name
                    string relativePath;
                    if (psb.Type == PsbType.Pimg && !string.IsNullOrWhiteSpace(resource.Name))
                    {
                        relativePath = Path.GetFileNameWithoutExtension(resource.Name);
                    }
                    else if (string.IsNullOrWhiteSpace(resource.Name) || string.IsNullOrWhiteSpace(resource.Part))
                    {
                        relativePath = resource.Index.ToString();
                    }
                    else
                    {
                        relativePath = $"{resource.Part}{PsbResCollector.ResourceNameDelimiter}{resource.Name}";
                    }

                    switch (imageOption)
                    {
                    case PsbImageOption.Extract:
                        //var pixelFormat = resource.Spec.DefaultPixelFormat(); //MARK: PixelFormat should refer `type`
                        switch (extractFormat)
                        {
                        case PsbImageFormat.Png:
                            relativePath += ".png";
                            if (resource.Compress == PsbCompressType.RL)
                            {
                                RL.UncompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                         resource.Height, resource.Width, PsbImageFormat.Png, resource.PixelFormat);
                            }
                            else if (resource.Compress == PsbCompressType.Tlg ||
                                     resource.Compress == PsbCompressType.ByName && resource.Name.EndsWith(".tlg", true, null))
                            {
                                TlgImageConverter converter = new TlgImageConverter();
                                using (var ms = new MemoryStream(resource.Data))
                                {
                                    BinaryReader br = new BinaryReader(ms);
                                    converter.Read(br).Save(Path.Combine(dirPath, relativePath), ImageFormat.Png);
                                }
                                //WARN: tlg is kept and recorded in resource json for compile
                                relativePath = Path.ChangeExtension(relativePath, ".tlg");
                                File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                            }
                            else if (resource.Compress == PsbCompressType.ByName)
                            {
                                relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name));
                                File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                            }
                            else
                            {
                                RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                      resource.Height, resource.Width, extractFormat, resource.PixelFormat);
                            }
                            break;

                        default:
                            relativePath += ".bmp";
                            if (resource.Compress == PsbCompressType.RL)
                            {
                                RL.UncompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                         resource.Height, resource.Width, PsbImageFormat.Bmp, resource.PixelFormat);
                            }
                            else if (resource.Compress == PsbCompressType.Tlg ||
                                     resource.Compress == PsbCompressType.ByName && resource.Name.EndsWith(".tlg", true, null))
                            {
                                TlgImageConverter converter = new TlgImageConverter();
                                using (var ms = new MemoryStream(resource.Data))
                                {
                                    BinaryReader br = new BinaryReader(ms);
                                    converter.Read(br).Save(Path.Combine(dirPath, relativePath), ImageFormat.Bmp);
                                }
                                //WARN: tlg is kept and recorded in resource json for compile
                                relativePath = Path.ChangeExtension(relativePath, ".tlg");
                                File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                            }
                            else if (resource.Compress == PsbCompressType.ByName)
                            {
                                relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name));
                                File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                            }
                            else
                            {
                                RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                      resource.Height, resource.Width, extractFormat, resource.PixelFormat);
                            }
                            break;
                        }
                        break;

                    case PsbImageOption.Original:
                        if (resources[i].Compress == PsbCompressType.RL)
                        {
                            relativePath += ".rl";
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        else if (resource.Compress == PsbCompressType.Tlg)
                        {
                            relativePath += ".tlg";
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        else
                        {
                            relativePath += ".raw";
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        break;

                    case PsbImageOption.Uncompress:
                        File.WriteAllBytes(Path.Combine(dirPath, relativePath),
                                           resources[i].Compress == PsbCompressType.RL
                                    ? RL.Uncompress(resource.Data)
                                    : resource.Data);
                        relativePath += ".raw";
                        break;

                    case PsbImageOption.Compress:
                        File.WriteAllBytes(Path.Combine(dirPath, relativePath),
                                           resources[i].Compress != PsbCompressType.RL
                                    ? RL.Compress(resource.Data)
                                    : resource.Data);
                        relativePath += ".rl";
                        break;

                    default:
                        throw new ArgumentOutOfRangeException(nameof(imageOption), imageOption, null);
                    }

                    try
                    {
                        resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}");
                    }
                    catch (ArgumentException e)
                    {
                        throw new BadImageFormatException("There are resources with same names! Try Raw export mode.", e);
                    }
                }
            }

            //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json`
            if (useResx)
            {
                resx.Resources = resDictionary;
                File.WriteAllText(inputPath + ".resx.json", JsonConvert.SerializeObject(resx, Formatting.Indented));
            }
            else
            {
                File.WriteAllText(inputPath + ".res.json", JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented));
            }
        }
Beispiel #15
0
        internal static void OutputResources(PSB psb, FreeMountContext context, string filePath, PsbExtractOption extractOption = PsbExtractOption.Original,
                                             PsbImageFormat extractFormat = PsbImageFormat.png, bool useResx = true)
        {
            var             name    = Path.GetFileNameWithoutExtension(filePath);
            var             dirPath = Path.Combine(Path.GetDirectoryName(filePath), name);
            PsbResourceJson resx    = new PsbResourceJson(psb, context.Context);

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            var extraDir = Path.Combine(dirPath, Consts.ExtraResourceFolderName);

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                if (psb.Resources.Count != 0)
                {
                    Directory.CreateDirectory(dirPath);
                }
            }

            if (psb.ExtraResources.Count > 0)
            {
                var extraDic = PsbResHelper.OutputExtraResources(psb, context, name, extraDir, out var flattenArrays, extractOption);
                resx.ExtraResources = extraDic;
                if (flattenArrays != null && flattenArrays.Count > 0)
                {
                    resx.ExtraFlattenArrays = flattenArrays;
                }
            }

            var resDictionary = psb.TypeHandler.OutputResources(psb, context, name, dirPath, extractOption);

            //MARK: We use `.resx.json` to distinguish from psbtools' `.res.json`
            if (useResx)
            {
                resx.Resources = resDictionary;
                resx.Context   = context.Context;
                string json;
                if (Consts.JsonArrayCollapse)
                {
                    json = ArrayCollapseJsonTextWriter.SerializeObject(resx);
                }
                else
                {
                    json = JsonConvert.SerializeObject(resx, Formatting.Indented);
                }
                File.WriteAllText(ChangeExtensionForOutputJson(filePath, ".resx.json"), json);
            }
            else
            {
                if (psb.ExtraResources.Count > 0)
                {
                    throw new NotSupportedException("PSBv4 cannot use legacy res.json format.");
                }
                File.WriteAllText(ChangeExtensionForOutputJson(filePath, ".res.json"),
                                  JsonConvert.SerializeObject(resDictionary.Values.ToList(), Formatting.Indented));
            }
        }
Beispiel #16
0
        public static void ConvertToImageFile(byte[] data, string path, int height, int width, PsbImageFormat format,
                                              PsbPixelFormat colorFormat = PsbPixelFormat.None)
        {
            var bmp = ConvertToImage(data, height, width, colorFormat);

            switch (format)
            {
            case PsbImageFormat.Bmp:
                bmp.Save(path, ImageFormat.Bmp);
                break;

            case PsbImageFormat.Png:
                bmp.Save(path, ImageFormat.Png);
                break;
            }
        }
Beispiel #17
0
        /// <summary>
        /// Split textures into parts and save to files
        /// </summary>
        /// <param name="psb">PSB</param>
        /// <param name="path">Save directory</param>
        /// <param name="option">Save option</param>
        /// <param name="imageFormat">Save format</param>
        /// <param name="pixelFormat">When save to PSB special formats, specific pixel format to use</param>
        public static void SplitTextureToFiles(this PSB psb, string path, PsbImageOption option = PsbImageOption.Extract, PsbImageFormat imageFormat = PsbImageFormat.Png, PsbPixelFormat pixelFormat = PsbPixelFormat.None)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            var source = (PsbDictionary)psb.Objects["source"];

            foreach (var texPair in source)
            {
                if (!(texPair.Value is PsbDictionary tex))
                {
                    continue;
                }
                var name = texPair.Key;
                if (!Directory.Exists(Path.Combine(path, name)))
                {
                    Directory.CreateDirectory(Path.Combine(path, name));
                }
                var icon    = (PsbDictionary)tex["icon"];
                var texture = (PsbDictionary)tex["texture"];

                //var mipmap = (PsbDictionary)texture["mipMap"]; //TODO: Mipmap?

                var md = PsbResCollector.GenerateMotionResMetadata(texture, (PsbResource)texture["pixel"]);
                md.Spec = psb.Platform; //Important
                Bitmap bmp = md.ToImage();

                foreach (var iconPair in icon)
                {
                    var    savePath = Path.Combine(path, name, iconPair.Key);
                    var    info     = (PsbDictionary)iconPair.Value;
                    var    width    = (int)(PsbNumber)info["width"];
                    var    height   = (int)(PsbNumber)info["height"];
                    var    top      = (int)(PsbNumber)info["top"];
                    var    left     = (int)(PsbNumber)info["left"];
                    var    attr     = (int)(PsbNumber)info["attr"];
                    Bitmap b        = new Bitmap(width, height, PixelFormat.Format32bppArgb);
#if USE_FASTBITMAP
                    using (FastBitmap f = b.FastLock())
                    {
                        f.CopyRegion(bmp, new Rectangle(left, top, width, height), new Rectangle(0, 0, b.Width, b.Height));
                    }
#else
                    Graphics g = Graphics.FromImage(b);
                    //g.InterpolationMode = InterpolationMode.NearestNeighbor;
                    //g.PixelOffsetMode = PixelOffsetMode.Half;
                    g.DrawImage(bmp, new Rectangle(0, 0, b.Width, b.Height), new Rectangle(left, top, width, height),
                                GraphicsUnit.Pixel);
                    g.Dispose();
#endif

                    switch (option)
                    {
                    case PsbImageOption.Decompress:
                        File.WriteAllBytes(savePath + ".raw", RL.GetPixelBytesFromImage(b, pixelFormat));
                        break;

                    case PsbImageOption.Compress:
                        File.WriteAllBytes(savePath + ".rl", RL.CompressImage(b, pixelFormat));
                        break;

                    case PsbImageOption.Original:
                    case PsbImageOption.Extract:
                    default:
                        switch (imageFormat)
                        {
                        case PsbImageFormat.Bmp:
                            b.Save(savePath + ".bmp", ImageFormat.Bmp);
                            break;

                        case PsbImageFormat.Png:
                        default:
                            b.Save(savePath + ".png", ImageFormat.Png);
                            //b.Save(savePath + $"_{attr}.png", ImageFormat.Png);
                            break;
                        }
                        break;
                    }
                }
                bmp.Dispose();
            }
        }
Beispiel #18
0
        public static void ConvertToImageFile(byte[] data, string path, int height, int width, PsbImageFormat format,
                                              PsbPixelFormat colorFormat = PsbPixelFormat.None, byte[] palette = null, PsbPixelFormat paletteColorFormat = PsbPixelFormat.None)
        {
            Bitmap bmp = ConvertToImage(data, palette, height, width, colorFormat, paletteColorFormat);

            switch (format)
            {
            case PsbImageFormat.bmp:
                bmp.Save(path, ImageFormat.Bmp);
                break;

            case PsbImageFormat.png:
                bmp.Save(path, ImageFormat.Png);
                break;
            }
        }
Beispiel #19
0
        /// <summary>
        /// Convert a PSB to External Texture PSB.
        /// </summary>
        /// <param name="inputPath"></param>
        /// <param name="outputUnlinkedPsb">output unlinked PSB if you need</param>
        /// <param name="order"></param>
        /// <param name="format"></param>
        public static void UnlinkToFile(string inputPath, bool outputUnlinkedPsb = true, PsbLinkOrderBy order = PsbLinkOrderBy.Name, PsbImageFormat format = PsbImageFormat.Png)
        {
            if (!File.Exists(inputPath))
            {
                return;
            }

            var name    = Path.GetFileNameWithoutExtension(inputPath);
            var dirPath = Path.Combine(Path.GetDirectoryName(inputPath), name);

            if (File.Exists(dirPath))
            {
                name    += "-resources";
                dirPath += "-resources";
            }

            if (!Directory.Exists(dirPath)) //ensure there is no file with same name!
            {
                Directory.CreateDirectory(dirPath);
            }

            var psb  = new PSB(inputPath);
            var texs = psb.Unlink();

            if (outputUnlinkedPsb)
            {
                psb.Merge();
                var psbSavePath = Path.ChangeExtension(inputPath, ".unlinked.psb");
                File.WriteAllBytes(psbSavePath, psb.Build());
            }

            var texExt    = format == PsbImageFormat.Bmp ? ".bmp" :".png";
            var texFormat = format == PsbImageFormat.Bmp ? ImageFormat.Bmp : ImageFormat.Png;

            switch (order)
            {
            case PsbLinkOrderBy.Convention:
                foreach (var tex in texs)
                {
                    tex.Save(Path.Combine(dirPath, tex.Tag + texExt), texFormat);
                }
                break;

            case PsbLinkOrderBy.Name:
                foreach (var tex in texs)
                {
                    tex.Save(Path.Combine(dirPath, $"{name}_{tex.Tag}{texExt}"), texFormat);
                }
                break;

            case PsbLinkOrderBy.Order:
                for (var i = 0; i < texs.Count; i++)
                {
                    var tex = texs[i];
                    tex.Save(Path.Combine(dirPath, $"{i}{texExt}"), texFormat);
                }
                break;
            }
        }
Beispiel #20
0
        public static Dictionary <string, string> OutputImageResources(PSB psb, FreeMountContext context, string name,
                                                                       string dirPath,
                                                                       PsbExtractOption extractOption = PsbExtractOption.Original,
                                                                       PsbImageFormat extractFormat   = PsbImageFormat.png)
        {
            var resources = psb.CollectResources <ImageMetadata>();

            Dictionary <string, string> resDictionary = new Dictionary <string, string>();

            ImageFormat pixelFormat;

            switch (extractFormat)
            {
            case PsbImageFormat.png:
                pixelFormat = ImageFormat.Png;
                break;

            default:
                pixelFormat = ImageFormat.Bmp;
                break;
            }

            if (extractOption == PsbExtractOption.Original)
            {
                for (int i = 0; i < psb.Resources.Count; i++)
                {
                    var relativePath = psb.Resources[i].Index == null ? $"#{Consts.ResourceIdentifierChar}{i}.bin" : $"{psb.Resources[i].Index}.bin";

                    File.WriteAllBytes(
                        Path.Combine(dirPath, relativePath),
                        psb.Resources[i].Data);
                    resDictionary.Add(Path.GetFileNameWithoutExtension(relativePath), $"{name}/{relativePath}");
                }
            }
            else
            {
                for (int i = 0; i < resources.Count; i++)
                {
                    var resource = resources[i];
                    //Generate Friendly Name
                    var    friendlyName = resource.GetFriendlyName(psb.Type);
                    string relativePath = friendlyName;
                    if (string.IsNullOrWhiteSpace(friendlyName))
                    {
                        relativePath = resource.Resource.Index?.ToString() ?? $"({i})";
                        friendlyName = i.ToString();
                    }

                    var currentExtractOption = extractOption;
                    if (resource.Compress != PsbCompressType.Tlg && resource.Compress != PsbCompressType.ByName && (resource.Width <= 0 || resource.Height <= 0)) //impossible to extract, just keep raw
                    {
                        if (currentExtractOption == PsbExtractOption.Extract)
                        {
                            currentExtractOption = PsbExtractOption.Original;
                        }
                    }

                    switch (currentExtractOption)
                    {
                    case PsbExtractOption.Extract:
                        switch (extractFormat)
                        {
                        case PsbImageFormat.png:
                            relativePath += ".png";
                            break;

                        default:
                            relativePath += ".bmp";
                            break;
                        }

                        relativePath = CheckPath(relativePath, i);
                        if (resource.Compress == PsbCompressType.RL)
                        {
                            RL.DecompressToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                     resource.Height, resource.Width, extractFormat, resource.PixelFormat);
                        }
                        else if (resource.Compress == PsbCompressType.Tlg ||
                                 resource.Compress == PsbCompressType.ByName)
                        {
                            var bmp = context.ResourceToBitmap(resource.Compress == PsbCompressType.Tlg
                                    ? ".tlg"
                                    : Path.GetExtension(resource.Name), resource.Data);
                            if (bmp == null)
                            {
                                if (resource.Compress == PsbCompressType.Tlg)     //Fallback to managed TLG decoder
                                {
                                    using var ms = new MemoryStream(resource.Data);
                                    using var br = new BinaryReader(ms);
                                    bmp          = new TlgImageConverter().Read(br);
                                    bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat);
                                    bmp.Dispose();
                                }

                                relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name));
                                File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                            }
                            else
                            {
                                bmp.Save(Path.Combine(dirPath, relativePath), pixelFormat);
                                bmp.Dispose();
                            }
                        }
                        //else if (resource.Compress == PsbCompressType.ByName)
                        //{
                        //    relativePath = Path.ChangeExtension(relativePath, Path.GetExtension(resource.Name));
                        //    File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        //}
                        else
                        {
                            RL.ConvertToImageFile(resource.Data, Path.Combine(dirPath, relativePath),
                                                  resource.Height, resource.Width, extractFormat, resource.PixelFormat, resource.PalData,
                                                  resource.PalettePixelFormat);
                        }

                        break;

                    case PsbExtractOption.Original:
                        if (resources[i].Compress == PsbCompressType.RL)
                        {
                            relativePath += ".rl";
                            relativePath  = CheckPath(relativePath, i);
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        else if (resource.Compress == PsbCompressType.Tlg)
                        {
                            relativePath += ".tlg";
                            relativePath  = CheckPath(relativePath, i);
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }
                        else
                        {
                            relativePath += ".raw";
                            relativePath  = CheckPath(relativePath, i);
                            File.WriteAllBytes(Path.Combine(dirPath, relativePath), resource.Data);
                        }

                        break;

                    //case PsbExtractOption.Decompress:
                    //    relativePath += ".raw";
                    //    relativePath = CheckPath(relativePath, i);
                    //    File.WriteAllBytes(Path.Combine(dirPath, relativePath),
                    //        resources[i].Compress == PsbCompressType.RL
                    //            ? RL.Decompress(resource.Data)
                    //            : resource.Data);
                    //    break;
                    //case PsbExtractOption.Compress:
                    //    relativePath += ".rl";
                    //    relativePath = CheckPath(relativePath, i);
                    //    File.WriteAllBytes(Path.Combine(dirPath, relativePath),
                    //        resources[i].Compress != PsbCompressType.RL
                    //            ? RL.Compress(resource.Data)
                    //            : resource.Data);
                    //    break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(currentExtractOption), currentExtractOption, null);
                    }

                    //Determine save name
                    try
                    {
                        bool indexConflict   = false;
                        uint conflictedIndex = 0;
                        if (resource.Resource.Index != null)                          //try index as name first
                        {
                            if (resDictionary.ContainsKey(resource.Index.ToString())) //index is used before
                            {
                                Console.WriteLine(
                                    $"[WARN] Resource Index {resource.Index} conflict. May be resource sharing, but may also be something wrong.");
                                //continue;
                                indexConflict           = true;
                                conflictedIndex         = resource.Resource.Index.Value;
                                resource.Resource.Index = null; //have another try on friendly name
                            }
                        }
                        if (resource.Resource.Index == null)
                        {
                            if (resDictionary.ContainsKey(friendlyName)) // friendly name is also used (most likely its the same res reused), no name can be used to save
                            {
                                Console.WriteLine(
                                    $"[WARN] Resource Name {friendlyName} conflict. May be resource sharing, but may also be something wrong.");
                                continue; //just skip
                            }

                            if (indexConflict) //index is used but friendly name is not (maybe they are different?), save using friendly name
                            {
                                Console.WriteLine($"[FIXED] Resource {friendlyName} is sharing same data with Index {conflictedIndex}");
                            }
                        }

                        resDictionary.Add(resource.Resource.Index == null ? friendlyName : resource.Index.ToString(),
                                          $"{name}/{relativePath}");
                    }
                    catch (ArgumentException e)
                    {
                        throw new PsbBadFormatException(PsbBadFormatReason.Resources,
                                                        "Resource Export Error: Name conflict, or Index is not specified. Try Raw export mode.", e);
                    }
                }
            }

            string CheckPath(string rPath, int id)
            {
                var k = Path.GetFileNameWithoutExtension(rPath);

                if (resDictionary.ContainsKey(k))
                {
                    return($"{id}{Path.GetExtension(rPath)}");
                }

                return(rPath);
            }

            return(resDictionary);
        }
Beispiel #21
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.");
        }