public byte[] GetFileFromIndex(int index)
        {
            FileListTab          file       = Table20.FileList[index];
            string               name       = file.Path.GetText();
            DirectoryListTab     dir        = file.Directory;
            DirectoryOffsetTable dirOffset  = dir.DirOffset;
            FileOffsetTab        offsetInfo = file.FileOffset;

            bool isLink = false;

            if (file.IsLink)
            {
                isLink = true;

                while (file.IsLink)
                {
                    file = file.FileOffset.File;
                }

                dir        = file.Directory;
                dirOffset  = dir.DirOffset;
                offsetInfo = file.FileOffset;
            }

            if (offsetInfo.Flag3)
            {
                dirOffset  = offsetInfo.LinkedDirOffset;
                offsetInfo = offsetInfo.LinkedOffset;
            }

            if (isLink)
            {
                return(new byte[0]);
            }
            if (offsetInfo.Size == 0)
            {
                return(new byte[0]);
            }

            long offset = Header.Field10 + dirOffset.Offset + offsetInfo.Offset * 4;

            var data = new byte[offsetInfo.Size];

            Stream.Position = offset;

            if (offsetInfo.SizeCompressed == 0 || offsetInfo.SizeCompressed == offsetInfo.Size)
            {
                Stream.Read(data, 0, offsetInfo.Size);
            }
            else
            {
                using (var compStream = new ZstandardStream(Stream, CompressionMode.Decompress, true))
                {
                    compStream.Read(data, 0, offsetInfo.Size);
                }
            }

            return(data);
        }
        public Table20(BinaryReader reader)
        {
            Header = new Table20Header(reader);

            StreamRoot = new StreamRootTable[Header.Field34];
            for (int i = 0; i < Header.Field34; i++)
            {
                StreamRoot[i] = new StreamRootTable(reader);
            }

            StreamHashToNameIndex = new StreamHashToNameIndexTab[Header.Field38];
            for (int i = 0; i < Header.Field38; i++)
            {
                StreamHashToNameIndex[i] = new StreamHashToNameIndexTab(reader);
            }

            StreamNameIndexToHash = new StreamNameIndexTab[Header.Field38];
            for (int i = 0; i < Header.Field38; i++)
            {
                StreamNameIndexToHash[i] = new StreamNameIndexTab(reader);
            }

            StreamIndexToFile = new StreamIndexToFileTab[Header.Field3C];
            for (int i = 0; i < Header.Field3C; i++)
            {
                StreamIndexToFile[i] = new StreamIndexToFileTab(reader);
            }

            StreamFiles = new StreamFilesTab[Header.StreamFileCount];
            for (int i = 0; i < Header.StreamFileCount; i++)
            {
                StreamFiles[i] = new StreamFilesTab(reader);
            }

            Field30 = new Tab20F30[Header.Field30];
            for (int i = 0; i < Header.Field30; i++)
            {
                Field30[i] = new Tab20F30(reader);
            }

            DirectoryList = new DirectoryListTab[Header.Field4];
            for (int i = 0; i < Header.Field4; i++)
            {
                DirectoryList[i] = new DirectoryListTab(reader, i);
            }

            DirectoryOffsets = new DirectoryOffsetTable[Header.Field20 + Header.Field8];
            for (int i = 0; i < Header.Field20 + Header.Field8; i++)
            {
                DirectoryOffsets[i] = new DirectoryOffsetTable(reader, i);
            }

            Field18 = new Tab20F18[Header.Field18];
            for (int i = 0; i < Header.Field18; i++)
            {
                Field18[i] = new Tab20F18(reader);
            }

            FileList = new FileListTab[Header.FieldC];
            for (int i = 0; i < Header.FieldC; i++)
            {
                FileList[i] = new FileListTab(reader, i);
            }

            FileOffsets = new FileOffsetTab[Header.Field24 + Header.Field10];
            for (int i = 0; i < Header.Field24 + Header.Field10; i++)
            {
                FileOffsets[i] = new FileOffsetTab(reader, i);
            }

            DirectoryListLookup = new DirectoryListLookupTab[Header.Field4];
            for (int i = 0; i < Header.Field4; i++)
            {
                DirectoryListLookup[i] = new DirectoryListLookupTab(reader, i);
            }

            int fileCount  = reader.ReadInt32();
            int groupCount = reader.ReadInt32();

            FieldXX = new Tab20Fxx[groupCount];
            for (int i = 0; i < groupCount; i++)
            {
                FieldXX[i] = new Tab20Fxx(reader);
            }

            FileListLookup = new FileListLookupTab[Header.Field14];
            for (int i = 0; i < Header.Field14; i++)
            {
                FileListLookup[i] = new FileListLookupTab(reader, i);
            }

            Field0CB = new Tab20F0CB[Header.FieldC];
            for (int i = 0; i < Header.FieldC; i++)
            {
                Field0CB[i] = new Tab20F0CB(reader);
            }

            HashSet <long> hashes = Hash.Hashes;

            foreach (var item in StreamRoot)
            {
                hashes.Add(item.Hash.GetHash());
            }

            foreach (var item in StreamHashToNameIndex)
            {
                hashes.Add(item.Hash.GetHash());
            }

            foreach (var item in StreamNameIndexToHash)
            {
                hashes.Add(item.Hash.GetHash());
            }

            foreach (var item in Field30)
            {
                hashes.Add(item.Hash.GetHash());
            }

            foreach (var item in DirectoryList)
            {
                hashes.Add(item.Path.GetHash());
                hashes.Add(item.Name.GetHash());
                hashes.Add(item.Parent.GetHash());
                hashes.Add(item.Hash4.GetHash());
            }

            foreach (var item in Field18)
            {
                hashes.Add(item.Hash.GetHash());
            }

            foreach (var item in FileList)
            {
                hashes.Add(item.Path.GetHash());
                hashes.Add(item.Extension.GetHash());
                hashes.Add(item.Parent.GetHash());
                hashes.Add(item.Name.GetHash());
            }

            foreach (var item in DirectoryListLookup)
            {
                hashes.Add(item.Hash.GetHash());
            }

            foreach (var item in FileListLookup)
            {
                hashes.Add(item.Hash.GetHash());
            }

            SetReferences();
        }
        public void ExtractFileIndex(int index, string outDir, IProgressReport progress, StringBuilder sb = null)
        {
            FileListTab          file       = Table20.FileList[index];
            string               name       = file.Path.GetText();
            DirectoryListTab     dir        = file.Directory;
            DirectoryOffsetTable dirOffset  = dir.DirOffset;
            FileOffsetTab        offsetInfo = file.FileOffset;

            bool isLink = false;

            long   offset = Header.Field10 + dirOffset.Offset + file.FileOffset.Offset * 4;
            string path;

            if (name != null)
            {
                path = Path.Combine(outDir, name);
            }
            else if (file.Parent.HasText())
            {
                path = Path.Combine(outDir, file.Parent.GetText(), index.ToString());
            }
            else
            {
                path = Path.Combine(outDir, "_", index.ToString());
            }

            if (file.IsLink)
            {
                isLink = true;

                while (file.IsLink)
                {
                    file = file.FileOffset.File;
                }

                dir        = file.Directory;
                dirOffset  = dir.DirOffset;
                offsetInfo = file.FileOffset;

                offset = Header.Field10 + dirOffset.Offset + offsetInfo.Offset * 4;
            }

            if (offsetInfo.Flag3)
            {
                dirOffset  = offsetInfo.LinkedDirOffset;
                offsetInfo = offsetInfo.LinkedOffset;

                offset = Header.Field10 + dirOffset.Offset + offsetInfo.Offset * 4;
            }

            //sb?.AppendLine($"{name}, 0x{file.Flags:x2}, 0x{dirOffset.Offset:x}, 0x{file.FileOffset.Offset:x}, 0x{offset:x}, 0x{file.FileOffset.SizeCompressed:x}, 0x{file.FileOffset.Size:x}, 0x{file.FileOffset.Flags:x2}, 0x{file.FileOffset.LinkFileIndex:x}, {file.Flag1}, {file.Flag9}, {file.Flag17}, {file.IsLink}, {file.Flag21}, {file.FileOffset.IsCompressed}, {file.FileOffset.Flag3}, {file.FileOffset.Flag4}, {file.FileOffset.Flag5}, {file.FileOffset.Flag6},");

            //return;

            try
            {
                //if (isLink) return;
                Directory.CreateDirectory(Path.GetDirectoryName(path));

                using (var fileOut = new FileStream(path, FileMode.Create, FileAccess.ReadWrite))
                //using (var fileOut = Stream.Null)
                {
                    Stream.Position = offset;

                    if (offsetInfo.Size == 0)
                    {
                        return;
                    }

                    if (offsetInfo.SizeCompressed == 0 || offsetInfo.SizeCompressed == offsetInfo.Size)
                    {
                        Stream.CopyStream(fileOut, offsetInfo.Size);
                    }
                    else
                    {
                        using (var compStream = new ZstandardStream(Stream, CompressionMode.Decompress, true))
                        {
                            compStream.CopyStream(fileOut, offsetInfo.Size);
                        }
                    }
                }

                sb?.AppendLine($"{name}, 0x{file.Flags:x2}, 0x{dirOffset.Offset:x}, 0x{offsetInfo.Offset:x}, 0x{offset:x}, 0x{offsetInfo.SizeCompressed:x}, 0x{offsetInfo.Size:x}, 0x{offsetInfo.Flags:x2}, 0x{offsetInfo.LinkFileIndex:x}, {file.Flag1}, {file.Flag9}, {file.Flag17}, {file.IsLink}, {file.Flag21}, {offsetInfo.IsCompressed}, {offsetInfo.Flag3}, {offsetInfo.Flag4}, {offsetInfo.Flag5}, {offsetInfo.Flag6}, ");
            }
            catch (InvalidDataException)
            {
                progress?.LogMessage($"File index 0x{file.Index:x5} Offset 0x{offset:x9}: Can't decompress {path}");
                try
                {
                    File.Delete(path);
                    sb?.AppendLine($"{name}, 0x{file.Flags:x2}, 0x{dirOffset.Offset:x}, 0x{offsetInfo.Offset:x}, 0x{offset:x}, 0x{offsetInfo.SizeCompressed:x}, 0x{offsetInfo.Size:x}, 0x{offsetInfo.Flags:x2}, 0x{offsetInfo.LinkFileIndex:x}, {file.Flag1}, {file.Flag9}, {file.Flag17}, {file.IsLink}, {file.Flag21}, {offsetInfo.IsCompressed}, {offsetInfo.Flag3}, {offsetInfo.Flag4}, {offsetInfo.Flag5}, {offsetInfo.Flag6}, X");

                    var badPath = Path.Combine(outDir, "bad", index.ToString());
                    Directory.CreateDirectory(Path.GetDirectoryName(badPath));

                    using (var fileOut = new FileStream(badPath, FileMode.Create, FileAccess.ReadWrite))
                    {
                        Stream.Position = offset;
                        var info = file.FileOffset;

                        if (info.Size == 0)
                        {
                            return;
                        }

                        Stream.CopyStream(fileOut, info.Size);
                    }
                }
                catch (Exception) { }
            }
            catch (Exception)
            {
                progress?.LogMessage($"File index 0x{file.Index:x5}: Bad path {path}");
                try
                {
                    File.Delete(path);
                }
                catch (Exception) { }
            }
        }