public void Save(string fileName)
        {
            var data   = new List <byte>();
            var header = new byte[32];

            Array.Copy(Encoding.UTF8.GetBytes("UNIF"), header, 4);
            header[4] = (byte)(Version & 0xFF);
            header[5] = (byte)((Version >> 8) & 0xFF);
            header[6] = (byte)((Version >> 16) & 0xFF);
            header[7] = (byte)((Version >> 24) & 0xFF);
            data.AddRange(header);

            var fields = new Dictionary <string, byte[]>(Fields);

            if (!fields.ContainsKey("DINF") && Version >= 2)
            {
                var dinf = new byte[204];
                var name = UnifFile.StringToUTF8N("Cluster / [email protected] / http://clusterrr.com");
                Array.Copy(name, dinf, name.Length);
                var dt = DateTime.Now;
                dinf[100] = (byte)dt.Month;
                dinf[101] = (byte)dt.Day;
                dinf[102] = (byte)(dt.Year & 0xFF);
                dinf[103] = (byte)(dt.Year >> 8);
                var software = UnifFile.StringToUTF8N("My own software and hardware");
                Array.Copy(software, 0, dinf, 104, software.Length);
                fields["DINF"] = dinf;
            }

            /*
             * if (Version >= 5)
             * {
             *  for (int p = 0; p < 16; p++)
             *  {
             *      if (fields.ContainsKey(string.Format("PRG{0:X1}", p)) && !fields.ContainsKey(string.Format("PCK{0:X1}", p)))
             *          fields[string.Format("PCK{0:X1}", p)] = CRC32(fields[string.Format("PRG{0:X1}", p)]);
             *      if (fields.ContainsKey(string.Format("CHR{0:X1}", p)) && !fields.ContainsKey(string.Format("CCK{0:X1}", p)))
             *          fields[string.Format("CCK{0:X1}", p)] = CRC32(fields[string.Format("CHR{0:X1}", p)]);
             *  }
             * }
             */

            foreach (var name in fields.Keys)
            {
                data.AddRange(Encoding.UTF8.GetBytes(name));
                int len = fields[name].Length;
                data.Add((byte)(len & 0xFF));
                data.Add((byte)((len >> 8) & 0xFF));
                data.Add((byte)((len >> 16) & 0xFF));
                data.Add((byte)((len >> 24) & 0xFF));
                data.AddRange(fields[name]);
            }

            File.WriteAllBytes(fileName, data.ToArray());
        }
        static void WriteCoolgirl(FamicomDumperConnection dumper, string fileName)
        {
            byte[] PRG;
            try
            {
                var nesFile = new NesFile(fileName);
                PRG = nesFile.PRG;
            }
            catch
            {
                var nesFile = new UnifFile(fileName);
                PRG = nesFile.Fields["PRG0"];
            }

            int prgBanks = PRG.Length / 0x8000;

            Console.Write("Reset... ");
            dumper.Reset();
            Console.WriteLine("OK");
            dumper.WriteCpu(0x5007, 0x04); // enable PRG write

            DateTime lastSectorTime = DateTime.Now;
            TimeSpan timeTotal      = new TimeSpan();

            for (int bank = 0; bank < prgBanks; bank++)
            {
                byte r0 = (byte)(bank >> 7);
                byte r1 = (byte)(bank << 1);
                dumper.WriteCpu(0x5000, r0);
                dumper.WriteCpu(0x5001, r1);
                dumper.WriteCpu(0x5002, 0xFE);

                var data = new byte[0x8000];
                int pos  = bank * 0x8000;
                if (pos % (128 * 1024) == 0)
                {
                    timeTotal      = new TimeSpan((DateTime.Now - lastSectorTime).Ticks * (prgBanks - bank) / 4);
                    timeTotal      = timeTotal.Add(DateTime.Now - startTime);
                    lastSectorTime = DateTime.Now;
                    Console.Write("Erasing sector... ");
                    dumper.ErasePrgFlash(FamicomDumperConnection.FlashType.Coolgirl);
                    Console.WriteLine("OK");
                }
                Array.Copy(PRG, pos, data, 0, data.Length);
                var timePassed = DateTime.Now - startTime;
                //var timeTotal = new TimeSpan((DateTime.Now - startTime).Ticks * prgBanks / (bank + 1));
                Console.Write("Writing {0}/{1} ({2}%, {3:D2}:{4:D2}:{5:D2}/{6:D2}:{7:D2}:{8:D2})... ", bank + 1, prgBanks, (int)(100 * bank / prgBanks),
                              timePassed.Hours, timePassed.Minutes, timePassed.Seconds, timeTotal.Hours, timeTotal.Minutes, timeTotal.Seconds);
                dumper.WritePrgFlash(0x0000, data, FamicomDumperConnection.FlashType.Coolgirl, true);
                Console.WriteLine("OK");
            }
        }
        public void Save(string fileName, string dumperName = null)
        {
            var data   = new List <byte>();
            var header = new byte[32];

            Array.Copy(Encoding.UTF8.GetBytes("UNIF"), header, 4);
            header[4] = (byte)(Version & 0xFF);
            header[5] = (byte)((Version >> 8) & 0xFF);
            header[6] = (byte)((Version >> 16) & 0xFF);
            header[7] = (byte)((Version >> 24) & 0xFF);
            data.AddRange(header);

            var fields = new Dictionary <string, byte[]>(Fields);

            if (!fields.ContainsKey("DINF") && Version >= 2)
            {
                var dinf = new byte[204];
                if (dumperName == null)
                {
                    dumperName = "Cluster / [email protected] / http://clusterrr.com";
                }
                var name = UnifFile.StringToUTF8N(dumperName);
                Array.Copy(name, dinf, name.Length);
                var dt = DateTime.Now;
                dinf[100] = (byte)dt.Month;
                dinf[101] = (byte)dt.Day;
                dinf[102] = (byte)(dt.Year & 0xFF);
                dinf[103] = (byte)(dt.Year >> 8);
                var software = UnifFile.StringToUTF8N("Famicom dumper by Cluster");
                Array.Copy(software, 0, dinf, 104, software.Length);
                fields["DINF"] = dinf;
            }

            foreach (var name in fields.Keys)
            {
                data.AddRange(Encoding.UTF8.GetBytes(name));
                int len = fields[name].Length;
                data.Add((byte)(len & 0xFF));
                data.Add((byte)((len >> 8) & 0xFF));
                data.Add((byte)((len >> 16) & 0xFF));
                data.Add((byte)((len >> 24) & 0xFF));
                data.AddRange(fields[name]);
            }

            File.WriteAllBytes(fileName, data.ToArray());
        }
        static void WriteCoolboy(FamicomDumperConnection dumper, string fileName)
        {
            byte[] PRG;
            try
            {
                var nesFile = new NesFile(fileName);
                PRG = nesFile.PRG;
            }
            catch
            {
                var nesFile = new UnifFile(fileName);
                PRG = nesFile.Fields["PRG0"];
            }
            while (PRG.Length < 512 * 1024)
            {
                var PRGbig = new byte[PRG.Length * 2];
                Array.Copy(PRG, 0, PRGbig, 0, PRG.Length);
                Array.Copy(PRG, 0, PRGbig, PRG.Length, PRG.Length);
                PRG = PRGbig;
            }

            int prgBanks = PRG.Length / 0x2000;

            Console.Write("Reset... ");
            dumper.Reset();
            Console.WriteLine("OK");
            dumper.WriteCpu(0xA001, 0x00); // RAM protect
            DateTime lastSectorTime = DateTime.Now;
            TimeSpan timeTotal      = new TimeSpan();

            for (int bank = 0; bank < prgBanks; bank += 2)
            {
                int  outbank = bank / 16;
                byte r0      = (byte)((outbank & 0x07) | ((outbank & 0xc0) >> 2));
                byte r1      = (byte)(((outbank & 0x30) >> 2) | ((outbank << 1) & 0x10));
                byte r2      = 0;
                byte r3      = 0;
                dumper.WriteCpu(0x6000, new byte[] { r0 });
                dumper.WriteCpu(0x6001, new byte[] { r1 });
                dumper.WriteCpu(0x6002, new byte[] { r2 });
                dumper.WriteCpu(0x6003, new byte[] { r3 });

                int inbank = bank % 64;
                dumper.WriteCpu(0x8000, new byte[] { 6, (byte)(inbank) });
                dumper.WriteCpu(0x8000, new byte[] { 7, (byte)(inbank | 1) });

                var data = new byte[0x4000];
                int pos  = bank * 0x2000;
                if (pos % (128 * 1024) == 0)
                {
                    timeTotal      = new TimeSpan((DateTime.Now - lastSectorTime).Ticks * (prgBanks - bank) / 16);
                    timeTotal      = timeTotal.Add(DateTime.Now - startTime);
                    lastSectorTime = DateTime.Now;
                    Console.Write("Erasing sector... ");
                    dumper.ErasePrgFlash(FamicomDumperConnection.FlashType.Coolboy);
                    Console.WriteLine("OK");
                }
                Array.Copy(PRG, pos, data, 0, data.Length);
                var timePassed = DateTime.Now - startTime;
                Console.Write("Writing {0}/{1} ({2}%, {3:D2}:{4:D2}:{5:D2}/{6:D2}:{7:D2}:{8:D2})... ", bank / 2 + 1, prgBanks / 2, (int)(100 * bank / prgBanks),
                              timePassed.Hours, timePassed.Minutes, timePassed.Seconds, timeTotal.Hours, timeTotal.Minutes, timeTotal.Seconds);
                dumper.WritePrgFlash(0x0000, data, FamicomDumperConnection.FlashType.Coolboy, true);
                Console.WriteLine("OK");
            }
        }
        static void Dump(FamicomDumperConnection dumper, string fileName, string mapperName, int prgSize, int chrSize, string unifName, string unifAuthor)
        {
            var mapper = GetMapper(mapperName);

            if (mapper.Number >= 0)
            {
                Console.WriteLine("Using mapper: #{0} ({1})", mapper.Number, mapper.Name);
            }
            else
            {
                Console.WriteLine("Using mapper: {0}", mapper.Name);
            }
            Console.WriteLine("Dumping...");
            List <byte> prg = new List <byte>();
            List <byte> chr = new List <byte>();

            prgSize = prgSize >= 0 ? prgSize : mapper.DefaultPrgSize;
            chrSize = chrSize >= 0 ? chrSize : mapper.DefaultChrSize;
            Console.WriteLine("PRG memory size: {0}K", prgSize / 1024);
            mapper.DumpPrg(dumper, prg, prgSize);
            while (prg.Count % 0x4000 != 0)
            {
                prg.Add(0);
            }
            Console.WriteLine("CHR memory size: {0}K", chrSize / 1024);
            mapper.DumpChr(dumper, chr, chrSize);
            while (chr.Count % 0x2000 != 0)
            {
                chr.Add(0);
            }
            byte[] mirroringRaw             = dumper.GetMirroring();
            NesFile.MirroringType mirroring = NesFile.MirroringType.Unknown_none;
            if (mirroringRaw.Length == 1)
            {
                mirroring = (NesFile.MirroringType)mirroringRaw[0];
                Console.WriteLine("Mirroring: " + mirroring);
            }
            else if (mirroringRaw.Length == 4)
            {
                switch (string.Format("{0:X2}{1:X2}{2:X2}{3:X2}", mirroringRaw[0], mirroringRaw[1], mirroringRaw[2], mirroringRaw[3]))
                {
                case "00000101":
                    mirroring = NesFile.MirroringType.Horizontal;     // Horizontal
                    break;

                case "00010001":
                    mirroring = NesFile.MirroringType.Vertical;     // Vertical
                    break;

                case "00000000":
                    mirroring = NesFile.MirroringType.OneScreenA;     // One-screen A
                    break;

                case "01010101":
                    mirroring = NesFile.MirroringType.OneScreenB;     // One-screen B
                    break;

                default:
                    mirroring = NesFile.MirroringType.Unknown_none;     // Unknown
                    break;
                }
                Console.WriteLine("Mirroring: {0} ({1:X2} {2:X2} {3:X2} {4:X2})", mirroring, mirroringRaw[0], mirroringRaw[1], mirroringRaw[2], mirroringRaw[3]);
            }
            Console.WriteLine("Saving to {0}...", fileName);
            if (mapper.Number >= 0)
            {
                var nesFile = new NesFile();
                nesFile.Mapper    = (byte)mapper.Number;
                nesFile.Mirroring = mirroring;
                nesFile.TvSystem  = NesFile.TvSystemType.Ntsc;
                nesFile.PRG       = prg.ToArray();
                nesFile.CHR       = chr.ToArray();
                nesFile.Save(fileName);
            }
            else
            {
                var unifFile = new UnifFile();
                unifFile.Version = 4;
                unifFile.Mapper  = mapper.UnifName;
                if (unifName != null)
                {
                    unifFile.Fields["NAME"] = UnifFile.StringToUTF8N(unifName);
                }
                unifFile.Fields["PRG0"] = prg.ToArray();
                if (chr.Count > 0)
                {
                    unifFile.Fields["CHR0"] = chr.ToArray();
                }
                unifFile.Fields["MIRR"] = new byte[] { 5 }; // mapper controlled
                unifFile.Save(fileName, unifAuthor);
            }
        }
        public static void Write(FamicomDumperConnection dumper, string fileName, IEnumerable <int> badSectors, bool silent, bool needCheck = false, bool writePBBs = false)
        {
            byte[] PRG;
            if (Path.GetExtension(fileName).ToLower() == ".bin")
            {
                PRG = File.ReadAllBytes(fileName);
            }
            else
            {
                try
                {
                    var nesFile = new NesFile(fileName);
                    PRG = nesFile.PRG;
                }
                catch
                {
                    var nesFile = new UnifFile(fileName);
                    PRG = nesFile.Fields["PRG0"];
                }
            }

            int prgBanks = PRG.Length / 0x4000;

            Console.Write("Reset... ");
            dumper.Reset();
            Console.WriteLine("OK");
            int flashSize = CommonHelper.GetFlashSize(dumper);

            if (PRG.Length > flashSize)
            {
                throw new Exception("This ROM is too big for this cartridge");
            }
            PPBErase(dumper);

            var writeStartTime = DateTime.Now;
            var lastSectorTime = DateTime.Now;
            var timeTotal      = new TimeSpan();
            int errorCount     = 0;

            for (int bank = 0; bank < prgBanks; bank++)
            {
                if (badSectors.Contains(bank / 8))
                {
                    bank += 8;                                // bad sector :(
                }
                try
                {
                    byte r0 = (byte)(((bank >> 3) & 0x07)          // 5, 4, 3 bits
                                     | (((bank >> 9) & 0x03) << 4) // 10, 9 bits
                                     | (1 << 6));                  // resets 4th mask bit
                    byte r1 = (byte)((((bank >> 7) & 0x03) << 2)   // 8, 7
                                     | (((bank >> 6) & 1) << 4)    // 6
                                     | (1 << 7));                  // resets 5th mask bit
                    byte r2 = 0;
                    byte r3 = (byte)((1 << 4)                      // NROM mode
                                     | ((bank & 7) << 1));         // 2, 1, 0 bits
                    dumper.WriteCpu(0x6000, new byte[] { r0 });
                    dumper.WriteCpu(0x6001, new byte[] { r1 });
                    dumper.WriteCpu(0x6002, new byte[] { r2 });
                    dumper.WriteCpu(0x6003, new byte[] { r3 });

                    var data = new byte[0x4000];
                    int pos  = bank * 0x4000;
                    if (pos % (128 * 1024) == 0)
                    {
                        timeTotal      = new TimeSpan((DateTime.Now - lastSectorTime).Ticks * (prgBanks - bank) / 8);
                        timeTotal      = timeTotal.Add(DateTime.Now - writeStartTime);
                        lastSectorTime = DateTime.Now;
                        Console.Write("Erasing sector... ");
                        dumper.ErasePrgFlash(FamicomDumperConnection.FlashAccessType.Direct);
                        Console.WriteLine("OK");
                    }
                    Array.Copy(PRG, pos, data, 0, data.Length);
                    var timePassed = DateTime.Now - writeStartTime;
                    Console.Write("Writing {0}/{1} ({2}%, {3:D2}:{4:D2}:{5:D2}/{6:D2}:{7:D2}:{8:D2})... ", bank + 1, prgBanks, (int)(100 * bank / prgBanks),
                                  timePassed.Hours, timePassed.Minutes, timePassed.Seconds, timeTotal.Hours, timeTotal.Minutes, timeTotal.Seconds);
                    dumper.WritePrgFlash(0x0000, data, FamicomDumperConnection.FlashAccessType.Direct, true);
                    Console.WriteLine("OK");
                    if ((bank % 8 == 7) || (bank == prgBanks - 1))
                    {
                        PPBWrite(dumper, (uint)bank / 8);
                    }
                }
                catch (Exception ex)
                {
                    errorCount++;
                    if (errorCount >= 3)
                    {
                        throw ex;
                    }
                    if (!silent)
                    {
                        Program.errorSound.PlaySync();
                    }
                    Console.WriteLine("Error: " + ex.Message);
                    bank = (bank & ~7) - 1;
                    Console.WriteLine("Lets try again");
                    Console.Write("Reset... ");
                    dumper.Reset();
                    Console.WriteLine("OK");
                    continue;
                }
            }
            if (errorCount > 0)
            {
                Console.WriteLine("Warning! Error count: {0}", errorCount);
            }

            if (needCheck)
            {
                Console.WriteLine("Starting check process");
                Console.Write("Reset... ");
                dumper.Reset();
                Console.WriteLine("OK");

                var readStartTime = DateTime.Now;
                lastSectorTime = DateTime.Now;
                timeTotal      = new TimeSpan();
                for (int bank = 0; bank < prgBanks; bank++)
                {
                    byte r0 = (byte)(((bank >> 3) & 0x07)          // 5, 4, 3 bits
                                     | (((bank >> 9) & 0x03) << 4) // 10, 9 bits
                                     | (1 << 6));                  // resets 4th mask bit
                    byte r1 = (byte)((((bank >> 7) & 0x03) << 2)   // 8, 7
                                     | (((bank >> 6) & 1) << 4)    // 6
                                     | (1 << 7));                  // resets 5th mask bit
                    byte r2 = 0;
                    byte r3 = (byte)((1 << 4)                      // NROM mode
                                     | ((bank & 7) << 1));         // 2, 1, 0 bits
                    dumper.WriteCpu(0x6000, new byte[] { r0 });
                    dumper.WriteCpu(0x6001, new byte[] { r1 });
                    dumper.WriteCpu(0x6002, new byte[] { r2 });
                    dumper.WriteCpu(0x6003, new byte[] { r3 });

                    var data = new byte[0x4000];
                    int pos  = bank * 0x4000;
                    if (pos % (128 * 1024) == 0)
                    {
                        timeTotal      = new TimeSpan((DateTime.Now - lastSectorTime).Ticks * (prgBanks - bank) / 8);
                        timeTotal      = timeTotal.Add(DateTime.Now - readStartTime);
                        lastSectorTime = DateTime.Now;
                    }
                    Array.Copy(PRG, pos, data, 0, data.Length);
                    UInt16 crc = 0;
                    foreach (var a in data)
                    {
                        crc ^= a;
                        for (int i = 0; i < 8; ++i)
                        {
                            if ((crc & 1) != 0)
                            {
                                crc = (UInt16)((crc >> 1) ^ 0xA001);
                            }
                            else
                            {
                                crc = (UInt16)(crc >> 1);
                            }
                        }
                    }
                    var timePassed = DateTime.Now - readStartTime;
                    Console.Write("Reading CRC {0}/{1} ({2}%, {3:D2}:{4:D2}:{5:D2}/{6:D2}:{7:D2}:{8:D2})... ", bank + 1, prgBanks, (int)(100 * bank / prgBanks),
                                  timePassed.Hours, timePassed.Minutes, timePassed.Seconds, timeTotal.Hours, timeTotal.Minutes, timeTotal.Seconds);
                    var crcr = dumper.ReadCpuCrc(0x8000, 0x4000);
                    if (crcr != crc)
                    {
                        throw new Exception(string.Format("Check failed: {0:X4} != {1:X4}", crcr, crc));
                    }
                    else
                    {
                        Console.WriteLine("OK (CRC = {0:X4})", crcr);
                    }
                }
                if (errorCount > 0)
                {
                    Console.WriteLine("Warning! Error count: {0}", errorCount);
                    return;
                }
            }
        }