protected List <MiloObject> GetEntries(MiloObjectDir miloDir)
        {
            var entries = new List <MiloObject>();

            GetEntries(miloDir, entries);
            return(entries);
        }
        protected MiloObjectDirEntry ParseWorldDir(AwesomeReader ar, MiloObjectDir dir)
        {
            // Read past unknown stuff, big hack
            int    unk1 = ar.ReadInt32();
            int    unk2 = ar.ReadInt32();
            float  unk3 = ar.ReadSingle();
            string unk4 = ar.ReadString();
            int    unk5 = ar.ReadInt32();
            int    unk6 = ar.ReadInt32();

            // Hack for project 9
            var dirEntry = new MiloObjectDirEntry()
            {
                Name = dir.Name
            };

            dirEntry.Version     = ar.ReadInt32();
            dirEntry.SubVersion  = ar.ReadInt32();
            dirEntry.ProjectName = ar.ReadString();

            // Skip matrices + constants
            var matCount = ar.ReadInt32(); // Usually 7

            ar.BaseStream.Position += (matCount * 48) + 9;

            // Read imported milos
            var importedMiloCount = ar.ReadInt32();

            dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                         .Select(x => ar.ReadString())
                                         .ToArray();

            // Boolean, true when sub directory?
            ar.ReadBoolean();

            // Sub directory names seem to be in reverse order of serialization...
            var subDirCount = ar.ReadInt32();
            var subDirNames = Enumerable.Range(0, subDirCount)
                              .Select(x => ar.ReadString())
                              .ToArray();

            // Read subdirectories
            foreach (var _ in subDirNames.Reverse())
            {
                var subDir = new MiloObjectDir();
                ReadFromStream(ar, subDir);

                dirEntry.SubDirectories.Add(subDir);
            }

            ar.BaseStream.Position += 17; // 0'd data + ADDE
            return(dirEntry);
        }
        protected MiloObject ParseDirEntryAsBlob(AwesomeReader ar, MiloObjectDir dir, string dirType)
        {
            // Reads data as a byte array
            var entrySize  = GuessEntrySize(ar);
            var entryBytes = new MiloObjectBytes(dirType)
            {
                Name = dir.Name
            };

            entryBytes.Data = ar.ReadBytes((int)entrySize);

            ar.BaseStream.Position += 4;
            return(entryBytes);
        }
        protected void GetEntries(MiloObjectDir miloDir, List <MiloObject> entries)
        {
            if (miloDir.Type != "ObjectDir")
            {
                throw new UnsupportedMiloException($"Directory type of \"{miloDir.Type}\" found, expected \"ObjectDir\" expected");
            }

            // Traverse sub directories
            if (!(miloDir.GetDirectoryEntry() is MiloObjectDirEntry dirEntry))
            {
                throw new UnsupportedMiloException("Could not parse directory entry");
            }

            foreach (var subDir in dirEntry.SubDirectories)
            {
                GetEntries(subDir, entries);
            }

            // Add entries in current directory
            entries.AddRange(miloDir.Entries);
        }
        public IActionResult ExtractFilesFromArk([FromBody] ScanRequest request, bool extractMilos, bool extractDTAs, bool convertTextures)
        {
            Archive ark;

            if (!System.IO.File.Exists(request.InputPath))
            {
                // Open as directory if available
                if (Directory.Exists(request.InputPath))
                {
                    ark = ArkFileSystem.FromDirectory(request.InputPath);
                }
                else
                {
                    return(BadRequest($"File \"{request.InputPath}\" does not exist!"));
                }
            }
            else
            {
                // Open as archive
                ark = ArkFile.FromFile(request.InputPath);
            }


            if (request.OutputPath == null)
            {
                return(BadRequest($"Output directory cannot be null!"));
            }

            string CombinePath(string basePath, string path)
            {
                // Consistent slash
                basePath = (request.OutputPath ?? "").Replace("/", "\\");
                path     = (path ?? "").Replace("/", "\\");

                Regex dotRegex = new Regex(@"[.]+[\\]");

                if (dotRegex.IsMatch(path))
                {
                    // Replaces dotdot path
                    path = dotRegex.Replace(path, x => $"({x.Value.Substring(0, x.Value.Length - 1)})\\");
                }

                return(Path.Combine(basePath, path));
            }

            string GetNonGenPath(string path)
            {
                // Consistent slash
                path = (path ?? "").Replace("/", "\\");

                Regex genRegex      = new Regex(@"gen\\[^\\]+$", RegexOptions.IgnoreCase);
                Regex platformRegex = new Regex(@"_[^_]+$", RegexOptions.IgnoreCase); // TODO: Revisit for Forge extensions

                if (genRegex.IsMatch(path))
                {
                    var splitPath = path.Split('\\');
                    var dir       = string.Join("\\", splitPath.SkipLast(2));
                    var file      = platformRegex.Replace(splitPath.Last(), "");

                    path = $"{dir}\\{file}";
                }

                return(path);
            }

            void SaveAsFile(MiloObjectBytes miloEntry, string basePath)
            {
                var fileName = SanitizeFileName(miloEntry.Name);
                var filePath = basePath = Path.Combine(basePath, miloEntry.Type, fileName);

                if (!Directory.Exists(Path.GetDirectoryName(filePath)))
                {
                    Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                }

                System.IO.File.WriteAllBytes(filePath, miloEntry.Data);
                Console.WriteLine($"Wrote \"{filePath}\"");
            }

            // Extract everything
            if (!extractDTAs && !extractMilos && !convertTextures)
            {
                foreach (var arkEntry in ark.Entries)
                {
                    var filePath = CombinePath(request.OutputPath, arkEntry.FullPath);

                    if (!Directory.Exists(Path.GetDirectoryName(filePath)))
                    {
                        Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                    }

                    using (var fs = System.IO.File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
                    {
                        using (var stream = ark.GetArkEntryFileStream(arkEntry))
                        {
                            stream.CopyTo(fs);
                        }
                    }

                    Console.WriteLine($"Wrote \"{filePath}\"");
                }

                return(Ok());
            }

            string SanitizeFileName(string fileName)
            {
                // Sanitize file name
                var invalidChars = new Regex($"[{new string(Path.GetInvalidFileNameChars())}]", RegexOptions.IgnoreCase);

                return(invalidChars.Replace(fileName, ""));
            }

            void ProcessMiloArkEntry(ArkEntry miloArkEntry)
            {
                var filePath = CombinePath(request.OutputPath, GetNonGenPath(miloArkEntry.FullPath));

                using (var stream = ark.GetArkEntryFileStream(miloArkEntry))
                {
                    var milo           = MiloFile.ReadFromStream(stream);
                    var miloSerializer = new MiloSerializer(new SystemInfo()
                    {
                        Version   = milo.Version,
                        BigEndian = milo.BigEndian,
                        Platform  = Enum.Parse <Platform>(request.Platform)
                    });

                    var miloDir = new MiloObjectDir();
                    using (var ms = new MemoryStream(milo.Data))
                    {
                        miloSerializer.ReadFromStream(ms, miloDir);
                    }

                    if (convertTextures)
                    {
                        var textureEntries = miloDir.Entries
                                             .Where(x => x.Type == "Tex")
                                             .Select(x => x is Tex ? x as Tex : miloSerializer.ReadFromMiloObjectBytes <Tex>(x as MiloObjectBytes))
                                             .Where(x => x.Bitmap != null && x.Bitmap.RawData?.Length > 0)
                                             .ToList();

                        if (textureEntries.Count <= 0)
                        {
                            return;
                        }

                        foreach (var texEntry in textureEntries)
                        {
                            var entryName = Path.GetFileNameWithoutExtension(SanitizeFileName(texEntry.Name));

                            var pngName = $"{entryName}.png";
                            var pngPath = Path.Combine(filePath, texEntry.Type, pngName);
                            texEntry.Bitmap.SaveAs(miloSerializer.Info, pngPath);

                            Console.WriteLine($"Wrote \"{pngPath}\"");

                            // Write DTA script to file
                            var scriptName = texEntry?.ScriptName ?? "";
                            if (texEntry.Script != null)
                            {
                                var dtaName = (string.IsNullOrEmpty(scriptName))
                                    ? $"{entryName}.dta"
                                    : $"{entryName}_{scriptName}.dta";

                                var dtaPath = Path.Combine(filePath, texEntry.Type, dtaName);

                                var parent = new ParentItem(ParentType.Default);
                                foreach (var item in texEntry.Script.Items)
                                {
                                    parent.Add(item);
                                }

                                texEntry.Script.Items.Clear();
                                texEntry.Script.Items.Add(parent);

                                System.IO.File.WriteAllText(dtaPath, texEntry.Script.ToString());
                                Console.WriteLine($"Wrote \"{dtaPath}\"");
                            }
                        }
                    }

                    if (extractMilos)
                    {
                        if (miloDir.Entries.Count <= 0)
                        {
                            return;
                        }

                        if (!Directory.Exists(Path.GetDirectoryName(filePath)))
                        {
                            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                        }

                        // Saves milo entries
                        miloDir.Entries.ForEach(x =>
                        {
                            SaveAsFile(x as MiloObjectBytes, filePath);
                            Console.WriteLine($"Wrote \"{filePath}\"");
                        });

                        if (miloDir.Extras.ContainsKey("DirectoryEntry"))
                        {
                            SaveAsFile(miloDir.Extras["DirectoryEntry"] as MiloObjectBytes, filePath);
                            Console.WriteLine($"Wrote \"{filePath}\"");
                        }
                    }
                }
            }

            void ProcessBitmapArkEntry(ArkEntry bitmapArkEntry)
            {
                var arkPath  = GetNonGenPath(bitmapArkEntry.FullPath);
                var filePath = CombinePath(request.OutputPath, $"{Path.GetDirectoryName(arkPath)}\\{Path.GetFileNameWithoutExtension(bitmapArkEntry.FileName)}.png");

                using (var stream = ark.GetArkEntryFileStream(bitmapArkEntry))
                {
                    var miloSerializer = new MiloSerializer(new SystemInfo()
                    {
                        Version   = 25,    // 10 = gh1, 24 = gh2
                        BigEndian = false, // Even on PPC it's LE
                        Platform  = Enum.Parse <Platform>(request.Platform)
                    });

                    var bitmap = miloSerializer.ReadFromStream <HMXBitmap>(stream);
                    bitmap.SaveAs(miloSerializer.Info, filePath);
                    Console.WriteLine($"Wrote \"{filePath}\"");
                }
            }

            // Extract milos
            if (extractMilos || convertTextures)
            {
                var miloEntries = ark.Entries.Where(x => _miloRegex.IsMatch(x.FullPath));

                Parallel.ForEach(miloEntries, (entry) =>
                {
                    ProcessMiloArkEntry(entry);
                });

                //foreach (var entry in miloEntries) ProcessMiloArkEntry(entry);
            }

            // Convert textures
            if (convertTextures)
            {
                var bitmapArkEntries = ark.Entries.Where(x => _bitmapRegex.IsMatch(x.FullPath));

                Parallel.ForEach(bitmapArkEntries, (entry) =>
                {
                    ProcessBitmapArkEntry(entry);
                });

                //foreach (var entry in bitmapArkEntries) ProcessBitmapArkEntry(entry);
            }

            return(Ok());
        }
        public override void ReadFromStream(AwesomeReader ar, ISerializable data)
        {
            var    dir = data as MiloObjectDir;
            int    version = ReadMagic(ar, data);
            string dirType = null, dirName = null;

            dir.Extras.Clear(); // Clears for good measure

            if (version >= 24)
            {
                // Parses directory type/name
                dirType = ar.ReadString();
                dirName = FileHelper.SanitizePath(ar.ReadString());

                //ar.BaseStream.Position += 8; // Skips string count + total length
                dir.Extras.Add("Num1", ar.ReadInt32());
                dir.Extras.Add("Num2", ar.ReadInt32());
            }

            int entryCount = ar.ReadInt32();
            var entries    = Enumerable.Range(0, entryCount).Select(x => new
            {
                Type = ar.ReadString(),
                Name = FileHelper.SanitizePath(ar.ReadString())
            }).ToArray();

            if (version == 10)
            {
                // Parses external resource paths?
                entryCount = ar.ReadInt32();

                // Note: Entry can be empty
                var external = Enumerable.Range(0, entryCount)
                               .Select(x => ar.ReadString())
                               .ToList();

                dir.Extras.Add("ExternalResources", external);
            }
            else if (version == 25 && dirType == "ObjectDir")
            {
                // Hack for project 9
                var dirEntry = new MiloObjectDirEntry()
                {
                    Name = dirName
                };
                dirEntry.Version     = ar.ReadInt32();
                dirEntry.SubVersion  = ar.ReadInt32();
                dirEntry.ProjectName = ar.ReadString();

                // Skip matrices + constants
                var matCount = ar.ReadInt32(); // Usually 7
                ar.BaseStream.Position += (matCount * 48) + 9;

                // Read imported milos
                var importedMiloCount = ar.ReadInt32();
                dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                             .Select(x => ar.ReadString())
                                             .ToArray();

                // Boolean, true when sub directory?
                ar.ReadBoolean();

                // Sub directory names seem to be in reverse order of serialization...
                var subDirCount = ar.ReadInt32();
                var subDirNames = Enumerable.Range(0, subDirCount)
                                  .Select(x => ar.ReadString())
                                  .ToArray();

                // Read subdirectories
                foreach (var _ in subDirNames.Reverse())
                {
                    var subDir = new MiloObjectDir();
                    ReadFromStream(ar, subDir);

                    dirEntry.SubDirectories.Add(subDir);
                }

                dir.Extras.Add("DirectoryEntry", dirEntry);
                ar.BaseStream.Position += 17; // 0'd data + ADDE
            }
            else if (version == 25 && (dirType == "WorldDir"))
            {
                // Read past unknown stuff, big hack
                int    unk1 = ar.ReadInt32();
                int    unk2 = ar.ReadInt32();
                float  unk3 = ar.ReadSingle();
                string unk4 = ar.ReadString();
                int    unk5 = ar.ReadInt32();
                int    unk6 = ar.ReadInt32();

                // Hack for project 9
                var dirEntry = new MiloObjectDirEntry()
                {
                    Name = dirName
                };
                dirEntry.Version     = ar.ReadInt32();
                dirEntry.SubVersion  = ar.ReadInt32();
                dirEntry.ProjectName = ar.ReadString();

                // Skip matrices + constants
                var matCount = ar.ReadInt32(); // Usually 7
                ar.BaseStream.Position += (matCount * 48) + 9;

                // Read imported milos
                var importedMiloCount = ar.ReadInt32();
                dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                             .Select(x => ar.ReadString())
                                             .ToArray();

                // Boolean, true when sub directory?
                ar.ReadBoolean();

                // Sub directory names seem to be in reverse order of serialization...
                var subDirCount = ar.ReadInt32();
                var subDirNames = Enumerable.Range(0, subDirCount)
                                  .Select(x => ar.ReadString())
                                  .ToArray();

                // Read subdirectories
                foreach (var _ in subDirNames.Reverse())
                {
                    var subDir = new MiloObjectDir();
                    ReadFromStream(ar, subDir);

                    dirEntry.SubDirectories.Add(subDir);
                }

                dir.Extras.Add("DirectoryEntry", dirEntry);
                ar.BaseStream.Position += 17; // 0'd data + ADDE
            }
            else if (version == 25 && (dirType == "RndDir" || dirType == "SynthDir"))
            {
                // Read past unknown stuff, big hack
                int unk1 = ar.ReadInt32();

                // Hack for project 9
                var dirEntry = new MiloObjectDirEntry()
                {
                    Name = dirName
                };
                dirEntry.Version     = ar.ReadInt32();
                dirEntry.SubVersion  = ar.ReadInt32();
                dirEntry.ProjectName = ar.ReadString();

                // Skip matrices + constants
                var matCount = ar.ReadInt32(); // Usually 7
                ar.BaseStream.Position += (matCount * 48) + 9;

                // Read imported milos
                var importedMiloCount = ar.ReadInt32();
                dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                             .Select(x => ar.ReadString())
                                             .ToArray();

                // Boolean, true when sub directory?
                ar.ReadBoolean();

                // Sub directory names seem to be in reverse order of serialization...
                var subDirCount = ar.ReadInt32();
                var subDirNames = Enumerable.Range(0, subDirCount)
                                  .Select(x => ar.ReadString())
                                  .ToArray();

                // Read subdirectories
                foreach (var _ in subDirNames.Reverse())
                {
                    var subDir = new MiloObjectDir();
                    ReadFromStream(ar, subDir);

                    dirEntry.SubDirectories.Add(subDir);
                }

                dir.Extras.Add("DirectoryEntry", dirEntry);
                ar.BaseStream.Position += 17; // 0'd data + ADDE
            }
            else if (version == 25 && (dirType == "PanelDir" || dirType == "VocalTrackDir" || dirType == "UILabelDir" || dirType == "UILabelDir" || dirType == "UIListDir" || dirType == "PitchArrowDir" || dirType == "Character"))
            {
                // Read past unknown stuff, big hack
                int unk1 = ar.ReadInt32();
                int unk2 = ar.ReadInt32();

                // Hack for project 9
                var dirEntry = new MiloObjectDirEntry()
                {
                    Name = dirName
                };
                dirEntry.Version     = ar.ReadInt32();
                dirEntry.SubVersion  = ar.ReadInt32();
                dirEntry.ProjectName = ar.ReadString();

                // Skip matrices + constants
                var matCount = ar.ReadInt32(); // Usually 7
                ar.BaseStream.Position += (matCount * 48) + 9;

                // Read imported milos
                var importedMiloCount = ar.ReadInt32();
                dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                             .Select(x => ar.ReadString())
                                             .ToArray();

                // Boolean, true when sub directory?
                ar.ReadBoolean();

                // Sub directory names seem to be in reverse order of serialization...
                var subDirCount = ar.ReadInt32();
                var subDirNames = Enumerable.Range(0, subDirCount)
                                  .Select(x => ar.ReadString())
                                  .ToArray();

                // Read subdirectories
                foreach (var _ in subDirNames.Reverse())
                {
                    var subDir = new MiloObjectDir();
                    ReadFromStream(ar, subDir);

                    dirEntry.SubDirectories.Add(subDir);
                }

                dir.Extras.Add("DirectoryEntry", dirEntry);
                ar.BaseStream.Position += 17; // 0'd data + ADDE
            }
            else if (version == 25 && (dirType == "BackgroundDir" || dirType == "WorldInstance"))
            {
                // Read past unknown stuff, big hack
                int unk1 = ar.ReadInt32();
                int unk2 = ar.ReadInt32();
                int unk3 = ar.ReadInt32();

                // Hack for project 9
                var dirEntry = new MiloObjectDirEntry()
                {
                    Name = dirName
                };
                dirEntry.Version     = ar.ReadInt32();
                dirEntry.SubVersion  = ar.ReadInt32();
                dirEntry.ProjectName = ar.ReadString();

                // Skip matrices + constants
                var matCount = ar.ReadInt32(); // Usually 7
                ar.BaseStream.Position += (matCount * 48) + 9;

                // Read imported milos
                var importedMiloCount = ar.ReadInt32();
                dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                             .Select(x => ar.ReadString())
                                             .ToArray();

                // Boolean, true when sub directory?
                ar.ReadBoolean();

                // Sub directory names seem to be in reverse order of serialization...
                var subDirCount = ar.ReadInt32();
                var subDirNames = Enumerable.Range(0, subDirCount)
                                  .Select(x => ar.ReadString())
                                  .ToArray();

                // Read subdirectories
                foreach (var _ in subDirNames.Reverse())
                {
                    var subDir = new MiloObjectDir();
                    ReadFromStream(ar, subDir);

                    dirEntry.SubDirectories.Add(subDir);
                }

                dir.Extras.Add("DirectoryEntry", dirEntry);
                ar.BaseStream.Position += 17; // 0'd data + ADDE
            }
            else if (version == 25 && (dirType == "TrackPanelDir" || dirType == "H2HTrackPanelDir"))
            {
                // Read past unknown stuff, big hack
                int unk1 = ar.ReadInt32();
                int unk2 = ar.ReadInt32();
                int unk3 = ar.ReadInt32();
                int unk4 = ar.ReadInt32();

                // Hack for project 9
                var dirEntry = new MiloObjectDirEntry()
                {
                    Name = dirName
                };
                dirEntry.Version     = ar.ReadInt32();
                dirEntry.SubVersion  = ar.ReadInt32();
                dirEntry.ProjectName = ar.ReadString();

                // Skip matrices + constants
                var matCount = ar.ReadInt32(); // Usually 7
                ar.BaseStream.Position += (matCount * 48) + 9;

                // Read imported milos
                var importedMiloCount = ar.ReadInt32();
                dirEntry.ImportedMiloPaths = Enumerable.Range(0, importedMiloCount)
                                             .Select(x => ar.ReadString())
                                             .ToArray();

                // Boolean, true when sub directory?
                ar.ReadBoolean();

                // Sub directory names seem to be in reverse order of serialization...
                var subDirCount = ar.ReadInt32();
                var subDirNames = Enumerable.Range(0, subDirCount)
                                  .Select(x => ar.ReadString())
                                  .ToArray();

                // Read subdirectories
                foreach (var _ in subDirNames.Reverse())
                {
                    var subDir = new MiloObjectDir();
                    ReadFromStream(ar, subDir);

                    dirEntry.SubDirectories.Add(subDir);
                }

                dir.Extras.Add("DirectoryEntry", dirEntry);
                ar.BaseStream.Position += 17; // 0'd data + ADDE
            }
            else if (version >= 24)
            {
                // GH2 and above

                // Reads data as a byte array
                var entrySize  = GuessEntrySize(ar);
                var entryBytes = new MiloObjectBytes(dirType)
                {
                    Name = dirName
                };
                entryBytes.Data = ar.ReadBytes((int)entrySize);

                dir.Extras.Add("DirectoryEntry", entryBytes);
                ar.BaseStream.Position += 4;
            }


            foreach (var entry in entries)
            {
                var entryOffset = ar.BaseStream.Position;
                // TODO: De-serialize entries

                //try
                //{
                //    var miloEntry = ReadFromStream(ar.BaseStream, entry.Type);
                //    miloEntry.Name = entry.Name;

                //    dir.Entries.Add(miloEntry);
                //    ar.BaseStream.Position += 4; // Skips padding
                //    continue;
                //}
                //catch (Exception ex)
                //{
                //    // Catch exception and log?
                //    ar.Basestream.Position = entryOffset; // Return to start
                //}

                // Reads data as a byte array
                var entrySize  = GuessEntrySize(ar);
                var entryBytes = new MiloObjectBytes(entry.Type)
                {
                    Name = entry.Name
                };
                entryBytes.Data = ar.ReadBytes((int)entrySize);

                dir.Entries.Add(entryBytes);
                ar.BaseStream.Position += 4;
            }
        }
Beispiel #7
0
        public static void ExportToGLTF(this MiloObjectDir milo, string path, AppState appState)
        {
            var serializer    = appState.GetSerializer();
            var pathDirectory = Path.GetDirectoryName(path);

            var textures = milo.Entries
                           .Where(x => "Tex".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase))
                           .Select(y => serializer.ReadFromMiloObjectBytes <Tex>(y as MiloObjectBytes))
                           .ToList();

            var extTexs = textures.Where(x => x.UseExternal).ToList();
            var miloDir = appState.GetWorkingDirectory().FullPath;

            // Update textures
            foreach (var texture in textures.Where(x => x.UseExternal && x.Bitmap == null)) // TODO: Figure out what UseExternal actually means
            {
                var texPath = Path.Combine(miloDir, MakeGenPath(texture.ExternalPath, appState.SystemInfo.Platform));
                var bitmap  = serializer.ReadFromFile <HMXBitmap>(texPath);

                texture.Bitmap      = bitmap;
                texture.UseExternal = false;
            }

            var views = milo.Entries
                        .Where(x => "View".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase))
                        .Select(y => serializer.ReadFromMiloObjectBytes <View>(y as MiloObjectBytes))
                        .ToList();

            views.AddRange(milo.Entries
                           .Where(x => "Group".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase))
                           .Select(y => serializer.ReadFromMiloObjectBytes <Group>(y as MiloObjectBytes))
                           .ToList());

            var meshes = milo.Entries
                         .Where(x => "Mesh".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase))
                         .Select(y => serializer.ReadFromMiloObjectBytes <Mackiloha.Render.Mesh>(y as MiloObjectBytes))
                         .Where(z => !string.IsNullOrEmpty(z.Material)) // Don't care about bone meshes for now
                         .ToList();

            var trans = milo.Entries
                        .Where(x => "Trans".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase))
                        .Select(y => serializer.ReadFromMiloObjectBytes <Mackiloha.Render.TransStandalone>(y as MiloObjectBytes))
                        .ToList();

            var materials = milo.Entries
                            .Where(x => "Mat".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase))
                            .Select(y => serializer.ReadFromMiloObjectBytes <Mackiloha.Render.Mat>(y as MiloObjectBytes))
                            //.Where(z => z.TextureEntries.Count > 0 && z.TextureEntries.Any(w => !string.IsNullOrEmpty(w.Texture))) // TODO: Idk?
                            .ToList();

            var cams = milo.Entries
                       .Where(x => "Cam".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase) && appState.SystemInfo.Version <= 10)
                       .Select(y => serializer.ReadFromMiloObjectBytes <Cam>(y as MiloObjectBytes))
                       .ToList();

            var environs = milo.Entries
                           .Where(x => "Environ".Equals(x.Type, StringComparison.CurrentCultureIgnoreCase) && appState.SystemInfo.Version <= 10)
                           .Select(y => serializer.ReadFromMiloObjectBytes <Environ>(y as MiloObjectBytes))
                           .ToList();

            var miloEntries = textures
                              .Union <MiloObject>(views)
                              .Union(meshes)
                              .Union(trans)
                              .Union(materials)
                              .Union(cams)
                              .Union(environs)
                              .ToList();

            var transEntries = miloEntries
                               .Where(x => x is ITrans)
                               .ToList();

            var drawEntries = miloEntries
                              .Where(x => x is IDraw)
                              .ToList();

            /* var transforms = milo.Entries
             *  .Where(x => x.Type.Equals("Trans", StringComparison.CurrentCultureIgnoreCase))
             *  .Select(y =>
             *  {
             *      var trans = MiloOG.Trans.FromStream(new MemoryStream((y as MiloEntry).Data));
             *      trans.Name = y.Name;
             *      return trans;
             *  }).ToList(); */

            var scene = new GLTF()
            {
                Asset = new Asset()
                {
                    Generator = $"Mackiloha v{typeof(MiloExtensions).Assembly.GetName().Version}"
                },
                Images = textures.Select(x => new Image()
                {
                    Name = Path.GetFileNameWithoutExtension(x.Name) + ".png",
                    Uri  = Path.GetFileNameWithoutExtension(x.Name) + ".png"
                }).ToArray(),
                Samplers = new Sampler[]
                {
                    new Sampler()
                    {
                        MagFilter = MagFilter.Linear,
                        MinFilter = MinFilter.Nearest,
                        WrapS     = WrapMode.Repeat,
                        WrapT     = WrapMode.Repeat
                    }
                },
                Scene = 0
            };

            var currentOffset = 0;

            scene.Textures = textures.Select(x => new Texture()
            {
                Name    = x.Name,
                Sampler = 0,
                Source  = currentOffset++
            }).ToArray();

            var keyIdxPairs = Enumerable.Range(0, textures.Count).ToDictionary(x => textures[x].Name);

            scene.Materials = materials.Select(x => new Material()
            {
                Name = x.Name,
                PbrMetallicRoughness = new PbrMetallicRoughness()
                {
                    BaseColorTexture =
                        // Verify material has texture and exists in milo
                        (x.TextureEntries.Any(w => !string.IsNullOrEmpty(w.Texture)) &&
                         keyIdxPairs.ContainsKey(x.TextureEntries.First(x => !string.IsNullOrEmpty(x.Texture)).Texture))
                            ? new BaseColorTexture()
                    {
                        // TODO: Figure out how to map multiple textures to single material
                        Index = keyIdxPairs[x.TextureEntries.First(y => !string.IsNullOrEmpty(y.Texture)).Texture]
                    }
                            : null,
                    BaseColorFactor = new Vector4 <double>(x.BaseColor.R, x.BaseColor.G, x.BaseColor.B, x.BaseColor.A),
                    MetallicFactor  = (x.TextureEntries.Any(w => !string.IsNullOrEmpty(w.Texture)) && x.TextureEntries.First(y => !string.IsNullOrEmpty(y.Texture)).Unknown2 == 2) ? 1 : 0,
                    RoughnessFactor = (x.TextureEntries.Any(w => !string.IsNullOrEmpty(w.Texture)) && x.TextureEntries.First(y => !string.IsNullOrEmpty(y.Texture)).Unknown2 == 2) ? 0 : 1
                },
                EmissiveFactor = new Vector3 <double>(),
                AlphaMode      = AlphaMode.Mask, // x.Blend == BlendFactor.One ? AlphaMode.Blend : AlphaMode.Opaque,
                DoubleSided    = true
            }).ToArray();

            if (!Directory.Exists(pathDirectory))
            {
                // Create directory
                Directory.CreateDirectory(pathDirectory);
            }

            // Saves textures
            for (int i = 0; i < textures.Count; i++)
            {
                textures[i].Bitmap.SaveAs(serializer.Info,
                                          Path.Combine(pathDirectory, scene.Images[i].Uri));
            }

            var accessors   = new List <Accessor>();
            var sceneMeshes = new List <GLTFTools.Mesh>();

            int bufferSize12 = meshes.Select(x => x.Vertices.Count * 12 * 2).Sum(); // Verts + norms
            int bufferSize8  = meshes.Select(x => x.Vertices.Count * 8).Sum();      // UV
            int bufferSize4  = meshes.Select(x => x.Faces.Count * 6).Sum();         // Faces

            if (bufferSize4 % 4 != 0)
            {
                bufferSize4 += 4 - (bufferSize4 % 4);
            }

            scene.Buffers = new GLTFTools.Buffer[]
            {
                new GLTFTools.Buffer()
                {
                    Name       = Path.GetFileNameWithoutExtension(path),
                    ByteLength = (bufferSize4 + bufferSize8 + bufferSize12),
                    Uri        = Path.GetFileNameWithoutExtension(path) + ".bin"
                }
            };

            scene.BufferViews = new BufferView[]
            {
                new BufferView()
                {
                    Name       = "vertsAndNorms",
                    ByteLength = bufferSize12,
                    ByteOffset = 0,
                    ByteStride = 12
                },
                new BufferView()
                {
                    Name       = "uvs",
                    ByteLength = bufferSize8,
                    ByteOffset = bufferSize12,
                    ByteStride = 8
                },
                new BufferView()
                {
                    Name       = "faces",
                    ByteLength = bufferSize4,
                    ByteOffset = bufferSize12 + bufferSize8,
                    ByteStride = null
                }
            };

            int buffer12Offset = scene.BufferViews[0].ByteOffset.Value;
            int buffer8Offset  = scene.BufferViews[1].ByteOffset.Value;
            int buffer4Offset  = scene.BufferViews[2].ByteOffset.Value;

            var bw = new BinaryWriter(new MemoryStream(new byte[bufferSize12 + bufferSize8 + bufferSize4]));
            Dictionary <string, int> meshIndex = new Dictionary <string, int>();

            currentOffset = 0;

            keyIdxPairs = Enumerable
                          .Range(0, materials.Count)
                          .ToDictionary(x => materials[x].Name);

            foreach (var mesh in meshes)
            {
                if (mesh.Vertices.Count <= 0 || mesh.Faces.Count <= 0)
                {
                    continue;
                }
                meshIndex.Add(mesh.Name, currentOffset++);

                // Finds related material + texture
                var mat = materials.First(x => ((string)x.Name).Equals(mesh.Material, StringComparison.CurrentCultureIgnoreCase));

                sceneMeshes.Add(new GLTFTools.Mesh()
                {
                    Name       = mesh.Name,
                    Primitives = new MeshPrimitive[]
                    {
                        new MeshPrimitive()
                        {
                            Attributes = new MeshPrimitiveAttributes()
                            {
                                Position           = accessors.Count,
                                Normal             = accessors.Count + 1,
                                TextureCoordinate0 = accessors.Count + 2
                            },
                            Indices  = accessors.Count + 3,
                            Material = keyIdxPairs[mesh.Material],
                            Mode     = RenderMode.Triangles
                        }
                    }
                });

                // Vertices
                accessors.Add(new Accessor()
                {
                    Name          = mesh.Name + "_positions",
                    ComponentType = ComponentType.Float,
                    Count         = mesh.Vertices.Count,
                    Min           = new double[]
                    {
                        mesh.Vertices.Select(x => x.X).Min(),
                        mesh.Vertices.Select(x => x.Y).Min(),
                        mesh.Vertices.Select(x => x.Z).Min()
                    },
                    Max = new double[]
                    {
                        mesh.Vertices.Select(x => x.X).Max(),
                        mesh.Vertices.Select(x => x.Y).Max(),
                        mesh.Vertices.Select(x => x.Z).Max()
                    },
                    Type       = GLType.Vector3,
                    BufferView = 0,
                    ByteOffset = buffer12Offset - scene.BufferViews[0].ByteOffset.Value
                });
                bw.BaseStream.Seek(buffer12Offset, SeekOrigin.Begin);
                foreach (var vert in mesh.Vertices)
                {
                    bw.Write(vert.X);
                    bw.Write(vert.Y);
                    bw.Write(vert.Z);
                }
                buffer12Offset = (int)bw.BaseStream.Position;

                // Normals
                accessors.Add(new Accessor()
                {
                    Name          = mesh.Name + "_normals",
                    ComponentType = ComponentType.Float,
                    Count         = mesh.Vertices.Count,
                    Min           = new double[]
                    {
                        mesh.Vertices.Select(x => x.NormalX).Min(),
                        mesh.Vertices.Select(x => x.NormalY).Min(),
                        mesh.Vertices.Select(x => x.NormalZ).Min()
                    },
                    Max = new double[]
                    {
                        mesh.Vertices.Select(x => x.NormalX).Max(),
                        mesh.Vertices.Select(x => x.NormalY).Max(),
                        mesh.Vertices.Select(x => x.NormalZ).Max()
                    },
                    Type       = GLType.Vector3,
                    BufferView = 0,
                    ByteOffset = buffer12Offset - scene.BufferViews[0].ByteOffset.Value
                });
                bw.BaseStream.Seek(buffer12Offset, SeekOrigin.Begin);
                foreach (var vert in mesh.Vertices)
                {
                    bw.Write(vert.NormalX);
                    bw.Write(vert.NormalY);
                    bw.Write(vert.NormalZ);
                }
                buffer12Offset = (int)bw.BaseStream.Position;

                // UV coordinates
                accessors.Add(new Accessor()
                {
                    Name          = mesh.Name + "_texcoords",
                    ComponentType = ComponentType.Float,
                    Count         = mesh.Vertices.Count,
                    Min           = new double[]
                    {
                        mesh.Vertices.Select(x => x.U).Min(),
                        mesh.Vertices.Select(x => x.V).Min()
                    },
                    Max = new double[]
                    {
                        mesh.Vertices.Select(x => x.U).Max(),
                        mesh.Vertices.Select(x => x.V).Max()
                    },
                    Type       = GLType.Vector2,
                    BufferView = 1,
                    ByteOffset = buffer8Offset - scene.BufferViews[1].ByteOffset.Value
                });
                bw.BaseStream.Seek(buffer8Offset, SeekOrigin.Begin);
                foreach (var vert in mesh.Vertices)
                {
                    bw.Write(vert.U);
                    bw.Write(vert.V);
                }
                buffer8Offset = (int)bw.BaseStream.Position;

                // Faces
                accessors.Add(new Accessor()
                {
                    Name          = mesh.Name + "_indicies",
                    ComponentType = ComponentType.UnsignedShort,
                    Count         = mesh.Faces.Count * 3,
                    Min           = new double[]
                    {
                        mesh.Faces.SelectMany(x => new [] { x.V1, x.V2, x.V3 }).Min()
                    },
                    Max = new double[]
                    {
                        mesh.Faces.SelectMany(x => new [] { x.V1, x.V2, x.V3 }).Max()
                    },
                    Type       = GLType.Scalar,
                    BufferView = 2,
                    ByteOffset = buffer4Offset - scene.BufferViews[2].ByteOffset.Value
                });
                bw.BaseStream.Seek(buffer4Offset, SeekOrigin.Begin);
                foreach (var face in mesh.Faces)
                {
                    bw.Write(face.V1);
                    bw.Write(face.V2);
                    bw.Write(face.V3);
                }
                buffer4Offset = (int)bw.BaseStream.Position;
            }

            scene.Accessors = accessors.ToArray();
            scene.Meshes    = sceneMeshes.ToArray();

            var nodes     = new List <Node>();
            var nodeIndex = new Dictionary <string, int>();

            /* // TODO: Make milo objects with transforms data
             * MiloOG.AbstractEntry GetAbstractEntry(string name)
             * {
             *  var entry = milo.Entries.FirstOrDefault(x => x.Name == name);
             *  if (entry == null) return null;
             *
             *  switch (entry.Type)
             *  {
             *      case "Mesh":
             *          return meshes.First(y => y.Name == entry.Name);
             *      case "Trans":
             *          return transforms.First(y => y.Name == entry.Name);
             *      case "View":
             *          return views.First(y => y.Name == entry.Name);
             *      default:
             *          return null;
             *  }
             * } */

            /* Matrix4<float>? GetTransform(string transform)
             * {
             *  var transEntry = milo.Entries.FirstOrDefault(y => y.Name == transform);
             *  if (transEntry == null) return null;
             *
             *  switch (transEntry.Type)
             *  {
             *      case "Mesh":
             *          var mesh = meshes.First(y => y.Name == transEntry.Name);
             *          return mesh.Mat2.ToGLMatrix();
             *      case "Trans":
             *          var trans = transforms.First(y => y.Name == transEntry.Name);
             *          return trans.Mat2.ToGLMatrix();
             *      case "View":
             *          var view2 = views.First(y => y.Name == transEntry.Name);
             *          return view2.Mat2.ToGLMatrix();
             *      default:
             *          return null;
             *  }
             * } */

            /* string GetTransformName(MiloOG.AbstractEntry entry)
             * {
             *  switch (entry.Type)
             *  {
             *      case "Mesh":
             *          var mesh = meshes.First(y => y.Name == entry.Name);
             *          return mesh.Transform;
             *      case "Trans":
             *          var trans = transforms.First(y => y.Name == entry.Name);
             *          return trans.Name;
             *      case "View":
             *          var view = views.First(y => y.Name == entry.Name);
             *          return view.Transform;
             *      default:
             *          return null;
             *  }
             * } */

            var shadowRegex = new System.Text.RegularExpressions.Regex("shadow[^.]*.mesh$", System.Text.RegularExpressions.RegexOptions.IgnoreCase);

            var children = drawEntries
                           .Where(w => w is ITrans) // Use trans collection?
                           .Select(x => new
            {
                Name  = (string)x.Name,
                Trans = (string)(x as ITrans).Transform
            })
                           .Where(y => !string.IsNullOrEmpty(y.Trans))
                           .GroupBy(z => z.Trans)
                           .ToDictionary(g => g.Key, g => g.Select(w => w.Name)
                                         .OrderBy(s => s)
                                         .Where(x => !shadowRegex.IsMatch(x)) // Removes shadow meshes for now
                                         .ToList());

            /* foreach (var entry in meshes.Union<MiloObject>(views)) // TODO: Union w/ trans
             * {
             *  var transName = (entry as Mackiloha.Render.Interfaces.ITrans)
             *      .Trans
             *      .Transform;
             *
             *  if (!children.ContainsKey(transName))
             *      children.Add(transName, new List<string>(new string[] { entry.Name }));
             *  else if (!children[transName].Contains(entry.Name))
             *      children[transName].Add(entry.Name);
             * } */

            var rootIndex = new List <int>();

            foreach (var key in children.Keys)
            {
                rootIndex.Add(nodes.Count);
                var transEntry = transEntries.FirstOrDefault(x => x.Name == key) as ITrans;

                var node = new Node()
                {
                    Name     = "Root_" + key,
                    Mesh     = meshIndex.ContainsKey(key) ? (int?)meshIndex[key] : null,
                    Matrix   = ToGLMatrix((transEntry != null) ? transEntry.Mat2 : Matrix4.Identity()),
                    Children = Enumerable.Range(nodes.Count + 1, children[key].Count).ToArray()
                };
                nodes.Add(node);

                foreach (var child in children[key])
                {
                    var subEntry = drawEntries.First(x => x.Name == child);

                    var subNode = new Node()
                    {
                        Name = subEntry.Name,
                        Mesh = meshIndex.ContainsKey(subEntry.Name) ? (int?)meshIndex[subEntry.Name] : null,
                        //Matrix = ToGLMatrix(subEntry.Mat1)
                    };
                    nodeIndex.Add(child, rootIndex.Last());
                    nodes.Add(subNode);
                }
            }

            int CreateNode(string name) // Returns index of node
            {
                if (nodeIndex.ContainsKey(name))
                {
                    return(nodeIndex[name]);
                }

                var           entry          = drawEntries.First(x => x.Name == name) as IDraw;
                var           transformEntry = drawEntries.First(x => x.Name == (entry as ITrans).Transform);
                List <string> subNodes       = entry.Drawables.Select(x => (string)x).ToList();

                var node = new Node()
                {
                    Name   = name,
                    Mesh   = meshIndex.ContainsKey(name) ? (int?)meshIndex[name] : null,
                    Matrix = ToGLMatrix((entry as ITrans).Mat1),
                    //Matrix = GetTransform(entry.Transform),
                    Children = (subNodes.Count > 0) ? subNodes.Select(x => CreateNode(x)).ToArray() : null
                };

                nodeIndex.Add(name, nodes.Count);
                nodes.Add(node);
                return(nodeIndex[name]);
            }

            // foreach (var n in meshes.Union<MiloOG.AbstractEntry>(views).Union<MiloOG.AbstractEntry>(transforms)) CreateNode(n.Name);
            //
            // scene.Scene = 0;
            // scene.Scenes = new Scene[] { new Scene() { Nodes = Enumerable.Range(0, nodes.Count).ToArray() } };


            // foreach (var view in views) CreateNode(view.Name);
            //
            // // Finds root node
            // var childrenNodes = nodes.SelectMany(x => x.Children ?? new int[0]).Distinct();
            // var parentNodes = Enumerable.Range(0, nodes.Count);
            // var rootIdx = parentNodes.Except(childrenNodes).Single();
            //
            // scene.Scene = 0;
            // scene.Scenes = new Scene[] { new Scene() { Nodes = new int[] { rootIdx } } };

            List <string> GetAllSubs(MiloObject entry)
            {
                List <string> subsEntriesNames = (entry as IDraw).Drawables
                                                 .Select(x => (string)x)
                                                 .ToList();

                var subEntries = subsEntriesNames
                                 .Select(x => drawEntries.FirstOrDefault(y => y.Name == x))
                                 .Where(y => y != null)
                                 .ToList();

                foreach (var subEntry in subEntries)
                {
                    subsEntriesNames.AddRange(GetAllSubs(subEntry));
                }

                return(subsEntriesNames);
            }

            scene.Scene = 0;
            //scene.Scenes = new Scene[] { new Scene() { Nodes = rootIndex.ToArray() } };

            scene.Scenes = views
                           .Select(x => new Scene()
            {
                Nodes = GetAllSubs(x)
                        .Select(y => nodeIndex.ContainsKey(y) ? nodeIndex[y] : -1)
                        .Where(z => z != -1)
                        .Distinct()
                        .ToArray()
            })
                           .OrderByDescending(x => x.Nodes.Length)
                           .ToArray();

            if (scene.Scenes.Length <= 0)
            {
                // Create scene from root notes
                scene.Scenes = new[]