// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : Retrode.cs // Author(s) : Natalia Portillo // // Component : Dumping SNES/MD/GEN/MS/N64/GB/GB/GBA carts with a Retrode. // // --[ Description ] ---------------------------------------------------------- // // Handles dumping using a Retrode. // // --[ License ] -------------------------------------------------------------- // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Linq; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.Core.Graphics; using Aaru.Logging; using Humanizer; using Humanizer.Bytes; using Humanizer.Localisation; using Version = Aaru.CommonTypes.Interop.Version; namespace Aaru.Core.Devices.Dumping; public partial class Dump { static readonly byte[] _sfcExtension = "SFC"u8.ToArray(); static readonly byte[] _genesisExtension = "BIN"u8.ToArray(); static readonly byte[] _smsExtension = "SMS"u8.ToArray(); static readonly byte[] _z64Extension = "Z64"u8.ToArray(); static readonly byte[] _gbExtension = "GB "u8.ToArray(); static readonly byte[] _gbcExtension = "GBC"u8.ToArray(); static readonly byte[] _gbaExtension = "GBA"u8.ToArray(); /// Dumps a game cartridge using a Retrode adapter void Retrode() { bool sense = _dev.Read10(out byte[] buffer, out _, 0, false, true, false, false, 0, 512, 0, 1, _dev.Timeout, out _); if(sense) { StoppingErrorMessage?.Invoke(Localization.Core.Could_not_read); return; } byte[] tmp = new byte[8]; Array.Copy(buffer, 0x36, tmp, 0, 8); // UMDs are stored inside a FAT16 volume if(!tmp.SequenceEqual(_fatSignature)) { StoppingErrorMessage?.Invoke(Localization.Core.Retrode_partition_not_recognized_not_dumping); return; } ushort fatStart = (ushort)((buffer[0x0F] << 8) + buffer[0x0E]); ushort sectorsPerFat = (ushort)((buffer[0x17] << 8) + buffer[0x16]); ushort rootStart = (ushort)(sectorsPerFat * 2 + fatStart); ushort rootSize = (ushort)(((buffer[0x12] << 8) + buffer[0x11]) * 32 / 512); byte sectorsPerCluster = buffer[0x0D]; UpdateStatus?.Invoke(string.Format(Localization.Core.Reading_root_directory_in_sector_0, rootStart)); sense = _dev.Read10(out buffer, out _, 0, false, true, false, false, rootStart, 512, 0, 1, _dev.Timeout, out _); if(sense) { StoppingErrorMessage?.Invoke(Localization.Core.Could_not_read); return; } int romPos; bool sfcFound = false; bool genesisFound = false; bool smsFound = false; bool n64Found = false; bool gbFound = false; bool gbcFound = false; bool gbaFound = false; tmp = new byte[3]; for(romPos = 0; romPos < buffer.Length; romPos += 0x20) { Array.Copy(buffer, romPos + 8, tmp, 0, 3); if(tmp.SequenceEqual(_sfcExtension)) { sfcFound = true; break; } if(tmp.SequenceEqual(_genesisExtension)) { genesisFound = true; break; } if(tmp.SequenceEqual(_smsExtension)) { smsFound = true; break; } if(tmp.SequenceEqual(_z64Extension)) { n64Found = true; break; } if(tmp.SequenceEqual(_gbExtension)) { gbFound = true; break; } if(tmp.SequenceEqual(_gbcExtension)) { gbcFound = true; break; } if(!tmp.SequenceEqual(_gbaExtension)) continue; gbaFound = true; break; } if(!sfcFound && !genesisFound && !smsFound && !n64Found && !gbaFound && !gbFound && !gbcFound) { StoppingErrorMessage?.Invoke(Localization.Core.No_cartridge_found_not_dumping); return; } ushort cluster = BitConverter.ToUInt16(buffer, romPos + 0x1A); uint romSize = BitConverter.ToUInt32(buffer, romPos + 0x1C); MediaType mediaType = gbaFound ? MediaType.GameBoyAdvanceGamePak : gbFound || gbcFound ? MediaType.GameBoyGamePak : n64Found ? MediaType.N64GamePak : smsFound ? MediaType.MasterSystemCartridge : genesisFound ? MediaType.MegaDriveCartridge : MediaType.SNESGamePak; uint blocksToRead = 64; double totalDuration = 0; double currentSpeed = 0; double maxSpeed = double.MinValue; double minSpeed = double.MaxValue; ReadOnlySpan senseBuf; if(_outputPlugin is not IByteAddressableImage outputBai || !outputBai.SupportedMediaTypes.Contains(mediaType)) { StoppingErrorMessage?.Invoke(Localization.Core .The_specified_format_does_not_support_the_inserted_cartridge); return; } sense = _dev.Read10(out byte[] readBuffer, out _, 0, false, true, false, false, 0, 512, 0, 1, _dev.Timeout, out _); if(sense) { StoppingErrorMessage?.Invoke(Localization.Core.Could_not_read); return; } uint startSector = (uint)(rootStart + rootSize + (cluster - 2) * sectorsPerCluster); uint romSectors = romSize / 512; uint romRemaining = romSize % 512; UpdateStatus?.Invoke(string.Format(Localization.Core.Cartridge_has_0_bytes_1, romSize, ByteSize.FromBytes(romSize).ToString("0.000"))); UpdateStatus?.Invoke(string.Format(Localization.Core.Media_identified_as_0, mediaType.Humanize())); ErrorNumber ret = outputBai.Create(_outputPath, mediaType, _formatOptions, romSize); // Cannot create image if(ret != ErrorNumber.NoError) { StoppingErrorMessage?.Invoke(Localization.Core.Error_creating_output_image_not_continuing + Environment.NewLine + outputBai.ErrorMessage); return; } if(_createGraph) _mediaGraph = new BlockMap((int)_dimensions, (int)_dimensions, romSectors); _dumpStopwatch.Restart(); double imageWriteDuration = 0; _speedStopwatch.Restart(); ulong sectorSpeedStart = 0; InitProgress?.Invoke(); for(ulong i = 0; i < romSectors; i += blocksToRead) { if(_aborted) { UpdateStatus?.Invoke(Localization.Core.Aborted); break; } if(romSectors - i < blocksToRead) blocksToRead = (uint)(romSectors - i); if(currentSpeed > maxSpeed && currentSpeed > 0) maxSpeed = currentSpeed; if(currentSpeed < minSpeed && currentSpeed > 0) minSpeed = currentSpeed; UpdateProgress?.Invoke(string.Format(Localization.Core.Reading_byte_0_of_1_2, i * 512, romSize, ByteSize.FromMegabytes(currentSpeed).Per(_oneSecond).Humanize()), (long)i * 512, romSize); _speedStopwatch.Start(); sense = _dev.Read10(out readBuffer, out senseBuf, 0, false, true, false, false, (uint)(startSector + i), 512, 0, (ushort)blocksToRead, _dev.Timeout, out double cmdDuration); _speedStopwatch.Stop(); totalDuration += cmdDuration; _writeStopwatch.Restart(); if(!sense && !_dev.Error) { outputBai.WriteBytes(readBuffer, 0, readBuffer.Length, out _); imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds; _mediaGraph.PaintSectorsGood(i, blocksToRead); } else { _errorLog?.WriteLine(i, _dev.Error, _dev.LastError, senseBuf); // TODO: Reset device after X errors if(_stopOnError) return; // TODO: Return more cleanly AaruLogging.WriteLine(Localization.Core.Skipping_0_bytes_from_errored_byte_1, _skip * 512, i * 512); i += _skip - blocksToRead; } _writeStopwatch.Stop(); sectorSpeedStart += blocksToRead; double elapsed = _speedStopwatch.Elapsed.TotalSeconds; if(elapsed <= 0 || sectorSpeedStart * 512 < 524288) continue; currentSpeed = sectorSpeedStart * 512 / (1048576 * elapsed); sectorSpeedStart = 0; _speedStopwatch.Reset(); } _speedStopwatch.Stop(); if(romRemaining > 0 && !_aborted) { if(currentSpeed > maxSpeed && currentSpeed > 0) maxSpeed = currentSpeed; if(currentSpeed < minSpeed && currentSpeed > 0) minSpeed = currentSpeed; UpdateProgress?.Invoke(string.Format(Localization.Core.Reading_byte_0_of_1_2, romSectors * 512, romSize, ByteSize.FromMegabytes(currentSpeed).Per(_oneSecond).Humanize()), (long)romSectors * 512, romSize); sense = _dev.Read10(out readBuffer, out senseBuf, 0, false, true, false, false, romSectors, 512, 0, 1, _dev.Timeout, out double cmdDuration); totalDuration += cmdDuration; if(!sense && !_dev.Error) { _writeStopwatch.Restart(); outputBai.WriteBytes(readBuffer, 0, (int)romRemaining, out _); imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds; _writeStopwatch.Stop(); } else { _errorLog?.WriteLine(romSectors, _dev.Error, _dev.LastError, senseBuf); // TODO: Reset device after X errors if(_stopOnError) return; // TODO: Return more cleanly AaruLogging.WriteLine(Localization.Core.Skipping_0_bytes_from_errored_byte_1, _skip * 512, romSectors * 512); } } EndProgress?.Invoke(); UpdateStatus?.Invoke(string.Format(Localization.Core.Dump_finished_in_0, _dumpStopwatch.Elapsed.Humanize(minUnit: TimeUnit.Second))); UpdateStatus?.Invoke(string.Format(Localization.Core.Average_dump_speed_0, ByteSize.FromBytes(512 * (romSectors + 1)) .Per(totalDuration.Milliseconds()) .Humanize())); UpdateStatus?.Invoke(string.Format(Localization.Core.Average_write_speed_0, ByteSize.FromBytes(512 * (romSectors + 1)) .Per(imageWriteDuration.Seconds()) .Humanize())); var metadata = new CommonTypes.Structs.ImageInfo { Application = "Aaru", ApplicationVersion = Version.GetInformationalVersion() }; if(!outputBai.SetImageInfo(metadata)) { ErrorMessage?.Invoke(Localization.Core.Error_0_setting_metadata + Environment.NewLine + outputBai.ErrorMessage); } // TODO: Set dump hardware //outputBAI.SetDumpHardware(); if(_preSidecar != null) outputBai.SetMetadata(_preSidecar); UpdateStatus?.Invoke(Localization.Core.Closing_output_file); _imageCloseStopwatch.Restart(); outputBai.Close(); _imageCloseStopwatch.Stop(); AaruLogging.WriteLine(Localization.Core.Closed_in_0, _imageCloseStopwatch.Elapsed.Humanize(minUnit: TimeUnit.Second)); if(_aborted) { UpdateStatus?.Invoke(Localization.Core.Aborted); return; } double totalChkDuration = 0; /* TODO: Create sidecar if(_metadata) WriteOpticalSidecar(blockSize, blocks, mediaType, null, null, 1, out totalChkDuration, null); */ UpdateStatus?.Invoke(""); UpdateStatus?.Invoke(string.Format(Localization.Core .Took_a_total_of_0_1_processing_commands_2_checksumming_3_writing_4_closing, _dumpStopwatch.Elapsed.Humanize(minUnit: TimeUnit.Second), totalDuration.Milliseconds().Humanize(minUnit: TimeUnit.Second), totalChkDuration.Milliseconds().Humanize(minUnit: TimeUnit.Second), imageWriteDuration.Seconds().Humanize(minUnit: TimeUnit.Second), _imageCloseStopwatch.Elapsed.Humanize(minUnit: TimeUnit.Second))); UpdateStatus?.Invoke(string.Format(Localization.Core.Average_speed_0, ByteSize.FromBytes(512 * (romSectors + 1)) .Per(totalDuration.Milliseconds()) .Humanize())); if(maxSpeed > 0) { UpdateStatus?.Invoke(string.Format(Localization.Core.Fastest_speed_burst_0, ByteSize.FromMegabytes(maxSpeed).Per(_oneSecond).Humanize())); } if(minSpeed is > 0 and < double.MaxValue) { UpdateStatus?.Invoke(string.Format(Localization.Core.Slowest_speed_burst_0, ByteSize.FromMegabytes(minSpeed).Per(_oneSecond).Humanize())); } UpdateStatus?.Invoke(""); Statistics.AddMedia(mediaType, true); } }