// Iterate through all rows under this ExDFile and execute given iterator function on each of them.
        public void Iterate(Action <ExDRow> iterator)
        {
            for (int i = 0; i < Rows.Count; i++)
            {
                ExDRow row = Rows[i];

                iterator(row);

                Program.ReportInnerProgress(((double)i + 1) / Rows.Count);
            }
        }
        // Decode ExD from the raw data and create new ExD instance.
        public ExDFile(SqFile sqFile, ExHFile exHFile)
        {
            Header = exHFile;

            // Read the raw data from SqFile.
            byte[] data = sqFile.ReadData();

            // Quit if no data is available.
            if (data == null || data.Length == 0)
            {
                return;
            }

            // Retrieve offset entries and total row sizes from the data header.
            int offsetEntriesSize = ToInt32(data, 0x8, true);
            int totalRowSize      = ToInt32(data, 0xc, true);

            // Copy offset entries from raw data for easier manipulation.
            // Offset entries start from 0x20.
            byte[] offsetEntries = new byte[offsetEntriesSize];
            Array.Copy(data, 0x20, offsetEntries, 0, offsetEntriesSize);

            // Copy all rows from raw data for easier manipulation.
            // Rows start right after offset entries.
            byte[] rawRows = new byte[totalRowSize];
            Array.Copy(data, 0x20 + offsetEntriesSize, rawRows, 0, totalRowSize);

            // Going through each offset entry.
            // Offset entry size is 0x8.
            for (int i = 0; i < offsetEntriesSize; i += 0x8)
            {
                ExDRow row = new ExDRow();
                row.Parent = this;

                // First 32 bits of the offset entry is the key for the row that we're reading.
                row.Key = ToInt32(offsetEntries, i, true);

                // Next 32 bits are the actual offset.
                row.Offset = ToInt32(offsetEntries, i + 0x4, true);

                // The offset includes the offset entries and 0x20 bytes for the raw header.
                // Actual position in the raw row byte array would be offset - raw header size - offset entries size.
                int rawRowPosition = row.Offset - 0x20 - offsetEntriesSize;

                // First 4 bytes in the raw row data is the size of the raw data.
                row.Size = ToInt32(rawRows, rawRowPosition, true);

                // Next 2 bytes are the check digit.
                row.CheckDigit = ToInt16(rawRows, rawRowPosition + 0x4, true);

                // After the size and the check digit, the raw data is actually divided into two sections.
                // First section is the fixed size data.
                // Copy it over to separate array for easier manipulation.
                byte[] fixedSizeData = new byte[exHFile.FixedSizeDataLength];
                Array.Copy(rawRows, rawRowPosition + 0x6, fixedSizeData, 0, exHFile.FixedSizeDataLength);

                // Finally, rest of the raw row is the second section which contains the additional data that are used for some columns with varying lengths. (i.e. string type)
                byte[] additionalData = new byte[row.Size - exHFile.FixedSizeDataLength];
                Array.Copy(rawRows, rawRowPosition + 0x6 + exHFile.FixedSizeDataLength, additionalData, 0, additionalData.Length);

                // Let's go through each column of this row now.
                foreach (ExHColumn column in exHFile.Columns)
                {
                    ExDData parsedData = new ExDData();

                    switch (column.Type)
                    {
                    // String type.
                    case 0x0:
                        // The first 4 bytes of the fixed size data contain the starting offset for the additional data section.
                        int valueStart = ToInt32(fixedSizeData, column.Offset, true);

                        // The additional data is 0-terminated.
                        int valueEnd = valueStart;
                        while (valueEnd < additionalData.Length && additionalData[valueEnd] != 0)
                        {
                            valueEnd++;
                        }

                        // Copy the value from additional data to new byte array for decoding.
                        byte[] field = new byte[valueEnd - valueStart];
                        Array.Copy(additionalData, valueStart, field, 0, field.Length);

                        // Decode it as string.
                        parsedData.Type  = typeof(string);
                        parsedData.Value = DecodeString(field);
                        break;

                    // Bool type.
                    case 0x1:
                        // The first byte of the fixed size data is the boolean value.
                        parsedData.Type  = typeof(bool);
                        parsedData.Value = fixedSizeData[column.Offset] != 0;
                        break;

                    // Signed byte type.
                    case 0x2:
                        // The first byte of the fixed size data is the value.
                        parsedData.Type  = typeof(sbyte);
                        parsedData.Value = (sbyte)fixedSizeData[column.Offset];
                        break;

                    // Byte type.
                    case 0x3:
                        // The first byte of the fixed size data is the value.
                        parsedData.Type  = typeof(byte);
                        parsedData.Value = fixedSizeData[column.Offset];
                        break;

                    // Short type.
                    case 0x4:
                        // The first 2 bytes of the fixed size data are the value.
                        parsedData.Type  = typeof(short);
                        parsedData.Value = ToInt16(fixedSizeData, column.Offset, true);
                        break;

                    // Ushort type.
                    case 0x5:
                        // The first 2 bytes of the fixed size data are the value.
                        parsedData.Type  = typeof(ushort);
                        parsedData.Value = ToUInt16(fixedSizeData, column.Offset, true);
                        break;

                    // Int type.
                    case 0x6:
                        // The first 4 bytes of the fixed size data are the value.
                        parsedData.Type  = typeof(int);
                        parsedData.Value = ToInt32(fixedSizeData, column.Offset, true);
                        break;

                    // Uint type.
                    case 0x7:
                        // The first 4 bytes of the fixed size data are the value.
                        parsedData.Type  = typeof(uint);
                        parsedData.Value = ToUInt32(fixedSizeData, column.Offset, true);
                        break;

                    // Float type.
                    case 0x9:
                        // The first 4 bytes of the fixed size data are the value.
                        parsedData.Type  = typeof(float);
                        parsedData.Value = ToSingle(fixedSizeData, column.Offset, true);
                        break;

                    // Long type.
                    case 0xb:
                        // The first 8 bytes of the fixed size data are the value.
                        parsedData.Type  = typeof(long);
                        parsedData.Value = ToInt64(fixedSizeData, column.Offset, true);
                        break;

                    // Masked boolean types.
                    // The first byte of the fixed size data contains the boolean value, where the bit offset is type value - 0x19 from right.
                    case 0x19:
                    case 0x1a:
                    case 0x1b:
                    case 0x1c:
                    case 0x1d:
                    case 0x1e:
                    case 0x1f:
                    case 0x20:
                        parsedData.Type  = typeof(bool);
                        parsedData.Value = (fixedSizeData[column.Offset] & (0x1 << (column.Type - 0x19))) != 0;
                        break;

                    // Unrecognized type.
                    default:
                        throw new Exception($"Unrecognized column type: 0x{column.Type.ToString("x")}");
                    }

                    row.Data.Add(column, parsedData);
                }

                Rows.Add(row);
            }
        }