using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Aaru.Checksums; using Aaru.CommonTypes; using Aaru.CommonTypes.AaruMetadata; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Database; using Aaru.Database.Models; using Aaru.Helpers; using Aaru.Logging; namespace Aaru.Images; [SuppressMessage("ReSharper", "UnusedType.Global")] public partial class Nes : IByteAddressableImage { int _chrLen; int _chrNvramLen; int _chrRamLen; byte[] _data; Stream _dataStream; ImageInfo _imageInfo; int _instRomLen; ushort _mapper; bool _nes20; NesHeaderInfo _nesHeaderInfo; bool _opened; int _prgLen; int _prgNvramLen; int _prgRamLen; int _promLen; byte _submapper; bool _trainer; #region IByteAddressableImage Members /// public string Author => Authors.NataliaPortillo; /// public Metadata AaruMetadata => null; /// public List DumpHardware => null; /// public string Format => _nes20 ? "NES 2.0" : "iNES"; /// public Guid Id => new("D597A3F4-2B1C-441C-8487-0BCABC509302"); /// // ReSharper disable once ConvertToAutoProperty public ImageInfo Info => _imageInfo; /// public string Name => Localization.Nes_Name; /// public bool Identify(IFilter imageFilter) { if(imageFilter == null) return false; Stream stream = imageFilter.GetDataForkStream(); // Not sure but seems to be a multiple of at least this if(stream.Length < 16) return false; stream.Position = 0; var magicBytes = new byte[4]; stream.EnsureRead(magicBytes, 0, 8); var magic = BitConverter.ToUInt32(magicBytes, 0); return magic == 0x1A53454E; } /// public ErrorNumber Open(IFilter imageFilter) { if(imageFilter == null) return ErrorNumber.NoSuchFile; Stream stream = imageFilter.GetDataForkStream(); // Not sure but seems to be a multiple of at least this, maybe more if(stream.Length < 16) return ErrorNumber.InvalidArgument; stream.Position = 0; var header = new byte[16]; stream.EnsureRead(header, 0, 8); var magic = BitConverter.ToUInt32(header, 0); if(magic != 0x1A53454E) return ErrorNumber.InvalidArgument; if((header[7] & 0x0C) == 0x08) _nes20 = true; _prgLen = header[4] * 16384; _chrLen = header[5] * 8192; _trainer = (header[6] & 0x4) != 0; _instRomLen = 0; _promLen = 0; int trainerLen = _trainer ? 512 : 0; _nesHeaderInfo = new NesHeaderInfo { NametableMirroring = (header[6] & 0x1) != 0, BatteryPresent = (header[6] & 0x2) != 0, FourScreenMode = (header[6] & 0x8) != 0, Mapper = (ushort)(header[6] >> 4 | header[7] & 0xF0) }; if((header[7] & 0x1) != 0) _nesHeaderInfo.ConsoleType = NesConsoleType.Vs; else if((header[7] & 0x2) != 0) _nesHeaderInfo.ConsoleType = NesConsoleType.Playchoice; else _nesHeaderInfo.ConsoleType = NesConsoleType.Nes; if(_nes20) { _nesHeaderInfo.ConsoleType = (NesConsoleType)(header[7] & 0x3); _nesHeaderInfo.Mapper += (ushort)((header[8] & 0xF) << 8); _nesHeaderInfo.Submapper = (byte)(header[8] >> 4); if((header[9] & 0xF) == 0xF) _prgLen = (1 << (header[4] >> 2)) * (header[4] & 0x3); else _prgLen += (header[9] & 0xF) * 16384; if(header[9] >> 4 == 0xF) _chrLen = (1 << (header[5] >> 2)) * (header[5] & 0x3); else _chrLen += (header[9] >> 4) * 8192; if((header[10] & 0xF) > 0) _prgRamLen = 64 << (header[10] & 0xF); if((header[10] & 0xF0) > 0) _prgNvramLen = 64 << ((header[10] & 0xF0) >> 4); if((header[11] & 0xF) > 0) _chrRamLen = 64 << (header[11] & 0xF); if((header[11] & 0xF0) > 0) _chrNvramLen = 64 << ((header[11] & 0xF0) >> 4); _nesHeaderInfo.TimingMode = (NesTimingMode)(header[12] & 0x3); switch(_nesHeaderInfo.ConsoleType) { case NesConsoleType.Vs: _nesHeaderInfo.VsPpuType = (NesVsPpuType)(header[13] & 0xF); _nesHeaderInfo.VsHardwareType = (NesVsHardwareType)(header[13] >> 4); break; case NesConsoleType.Extended: _nesHeaderInfo.ExtendedConsoleType = (NesExtendedConsoleType)(header[13] & 0xF); break; } _nesHeaderInfo.DefaultExpansionDevice = (NesDefaultExpansionDevice)(header[15] & 0x3F); } _data = new byte[imageFilter.DataForkLength - 16]; stream.Position = 16; stream.EnsureRead(_data, 0, _data.Length); _imageInfo = new ImageInfo { Application = "iNES", CreationTime = imageFilter.CreationTime, ImageSize = (ulong)imageFilter.DataForkLength, LastModificationTime = imageFilter.LastWriteTime, Sectors = (ulong)imageFilter.DataForkLength, MetadataMediaType = MetadataMediaType.LinearMedia, MediaType = MediaType.FamicomGamePak }; StringBuilder sb = new(); sb.AppendFormat(Localization.PRG_ROM_size_0_bytes, _prgLen).AppendLine(); sb.AppendFormat(Localization.CHR_ROM_size_0_bytes, _chrLen).AppendLine(); sb.AppendFormat(Localization.Trainer_size_0_bytes, trainerLen).AppendLine(); sb.AppendFormat(Localization.Mapper_0, _nesHeaderInfo.Mapper).AppendLine(); if(_nesHeaderInfo.BatteryPresent) sb.AppendLine(Localization.Has_battery_backed_RAM); if(_nesHeaderInfo.FourScreenMode) sb.AppendLine(Localization.Uses_four_screen_VRAM); else if(_nesHeaderInfo.NametableMirroring) sb.AppendLine(Localization.Uses_vertical_mirroring); else sb.AppendLine(Localization.Uses_horizontal_mirroring); switch(_nesHeaderInfo.ConsoleType) { // TODO: Proper media types case NesConsoleType.Vs: sb.AppendLine(Localization.VS_Unisystem_game); break; case NesConsoleType.Playchoice: sb.AppendLine(Localization.PlayChoice_10_game); sb.AppendFormat(Localization.INST_ROM_size_0_bytes, _instRomLen & 0xF).AppendLine(); sb.AppendFormat(Localization.PROM_size_0_bytes, _promLen).AppendLine(); break; case NesConsoleType.Nes: break; case NesConsoleType.Extended: switch(_nesHeaderInfo.ExtendedConsoleType) { case NesExtendedConsoleType.Vs: sb.AppendLine(Localization.VS_Unisystem_game); break; case NesExtendedConsoleType.Normal: break; case NesExtendedConsoleType.Playchoice: sb.AppendLine(Localization.PlayChoice_10_game); sb.AppendFormat(Localization.INST_ROM_size_0_bytes, _instRomLen & 0xF).AppendLine(); sb.AppendFormat(Localization.PROM_size_0_bytes, _promLen).AppendLine(); break; case NesExtendedConsoleType.VT01_Monochrome: case NesExtendedConsoleType.VT01: sb.AppendLine(Localization.VR_Technology_VT01); break; case NesExtendedConsoleType.VT02: sb.AppendLine(Localization.VR_Technology_VT02); break; case NesExtendedConsoleType.VT03: sb.AppendLine(Localization.VR_Technology_VT03); break; case NesExtendedConsoleType.VT09: sb.AppendLine(Localization.VR_Technology_VT09); break; case NesExtendedConsoleType.VT32: sb.AppendLine(Localization.VR_Technology_VT32); break; case NesExtendedConsoleType.VT369: sb.AppendLine(Localization.VR_Technology_VT369); break; } break; } _imageInfo.Comments = sb.ToString(); _opened = true; return ErrorNumber.NoError; } /// public string ErrorMessage { get; private set; } /// public bool IsWriting { get; private set; } /// public IEnumerable KnownExtensions => [".nes"]; /// public IEnumerable SupportedMediaTags => []; /// public IEnumerable SupportedMediaTypes => [MediaType.NESGamePak, MediaType.FamicomGamePak]; /// public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => []; /// public IEnumerable SupportedSectorTags => []; /// public bool Create(string path, MediaType mediaType, Dictionary options, ulong sectors, uint negativeSectors, uint overflowSectors, uint sectorSize) => Create(path, mediaType, options, (long)sectors) == ErrorNumber.NoError; /// public bool Close() { if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return false; } if(!IsWriting) { ErrorMessage = Localization.Image_is_not_opened_for_writing; return false; } var header = new byte[16]; if(_nesHeaderInfo is null) { string hash = Sha256Context.Data(_data, out _).ToLowerInvariant(); using var ctx = AaruContext.Create(Settings.Settings.MainDbPath); _nesHeaderInfo = ctx.NesHeaders.FirstOrDefault(hdr => hdr.Sha256 == hash); } _nesHeaderInfo ??= new NesHeaderInfo { Mapper = _mapper, ConsoleType = _instRomLen > 0 ? NesConsoleType.Playchoice : NesConsoleType.Nes, Submapper = _submapper }; header[0] = 0x4E; header[1] = 0x45; header[2] = 0x53; header[3] = 0x1A; header[4] = (byte)(_prgLen / 16384 & 0xFF); header[5] = (byte)(_chrLen / 8192 & 0xFF); header[6] = (byte)((_nesHeaderInfo.Mapper & 0xF) << 4); if(_nesHeaderInfo.FourScreenMode) header[6] |= 0x8; if(_trainer) header[6] |= 0x4; if(_nesHeaderInfo.BatteryPresent) header[6] |= 0x2; if(_nesHeaderInfo.NametableMirroring) header[6] |= 0x1; header[7] = (byte)(_mapper & 0xF0 | 0x8); header[7] |= (byte)_nesHeaderInfo.ConsoleType; header[8] = (byte)(_nesHeaderInfo.Submapper << 4 | (_nesHeaderInfo.Mapper & 0xF00) >> 4); header[9] = (byte)(_prgLen / 16384 >> 8); header[9] |= (byte)(_chrLen / 8192 >> 4 & 0xF); // TODO: PRG-RAM, PRG-NVRAM, CHR-RAM and CHR-NVRAM sizes header[12] = (byte)_nesHeaderInfo.TimingMode; header[13] = _nesHeaderInfo.ConsoleType switch { NesConsoleType.Vs => (byte)((int)_nesHeaderInfo.VsHardwareType << 4 | (int)_nesHeaderInfo.VsPpuType), NesConsoleType.Extended => (byte)_nesHeaderInfo.ExtendedConsoleType, _ => header[13] }; header[14] = 0; if(_instRomLen > 0) header[14]++; if(_promLen > 0) header[14]++; if(_nesHeaderInfo.ExtendedConsoleType == NesExtendedConsoleType.VT369) header[14]++; switch(_nesHeaderInfo.Mapper) { case 86 when _nesHeaderInfo.Submapper == 1: case 355: header[14]++; break; } header[15] = (byte)_nesHeaderInfo.DefaultExpansionDevice; _dataStream.Position = 0; _dataStream.Write(_data, 0, _data.Length); _dataStream.Close(); IsWriting = false; _opened = false; return true; } /// public bool SetMetadata(Metadata metadata) => false; /// public bool SetDumpHardware(List dumpHardware) => false; /// public bool SetImageInfo(ImageInfo imageInfo) => true; /// public long Position { get; set; } /// public ErrorNumber Create(string path, MediaType mediaType, Dictionary options, long maximumSize) { if(_opened) { ErrorMessage = Localization.Cannot_create_an_opened_image; return ErrorNumber.InvalidArgument; } if(mediaType != MediaType.FamicomGamePak && mediaType != MediaType.NESGamePak) { ErrorMessage = string.Format(Localization.Unsupported_media_format_0, mediaType); return ErrorNumber.NotSupported; } _imageInfo = new ImageInfo { MediaType = mediaType, Sectors = (ulong)maximumSize }; try { _dataStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); } catch(IOException ex) { ErrorMessage = string.Format(Localization.Could_not_create_new_image_file_exception_0, ex.Message); AaruLogging.Exception(ex, Localization.Could_not_create_new_image_file_exception_0, ex.Message); return ErrorNumber.InOutError; } _imageInfo.MediaType = mediaType; IsWriting = true; _opened = true; _data = new byte[maximumSize]; _nes20 = true; return ErrorNumber.NoError; } /// public ErrorNumber GetMappings(out LinearMemoryMap mappings) { mappings = new LinearMemoryMap(); if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return ErrorNumber.NotOpened; } List devices = [ new() { Type = LinearMemoryType.ROM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_prgLen } } ]; if(_chrLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.CharacterROM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_chrLen } }); } if(_trainer) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.Trainer }); } if(_instRomLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.ROM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_instRomLen } }); } if(_promLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.ROM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_promLen } }); } if(_prgRamLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.WorkRAM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_prgRamLen } }); } if(_chrRamLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.CharacterRAM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_chrRamLen } }); } if(_prgNvramLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.SaveRAM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_prgNvramLen } }); } if(_chrNvramLen > 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.CharacterRAM, PhysicalAddress = new LinearMemoryAddressing { Length = (ulong)_chrNvramLen } }); } ushort mapper = _nesHeaderInfo?.Mapper ?? _mapper; byte submapper = _nesHeaderInfo?.Submapper ?? _submapper; devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.Mapper, Description = $"NES Mapper {mapper}" }); if(submapper != 0) { devices.Add(new LinearMemoryDevice { Type = LinearMemoryType.Mapper, Description = $"NES Submapper {submapper}" }); } mappings = new LinearMemoryMap { Devices = devices.ToArray() }; return ErrorNumber.NoError; } /// public ErrorNumber ReadByte(out byte b, bool advance = true) => ReadByteAt(Position, out b, advance); /// public ErrorNumber ReadByteAt(long position, out byte b, bool advance = true) { b = 0; if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return ErrorNumber.NotOpened; } if(position >= _data.Length) { ErrorMessage = Localization.The_requested_position_is_out_of_range; return ErrorNumber.OutOfRange; } b = _data[position]; if(advance) Position = position + 1; return ErrorNumber.NoError; } /// public ErrorNumber ReadBytes(byte[] buffer, int offset, int bytesToRead, out int bytesRead, bool advance = true) => ReadBytesAt(Position, buffer, offset, bytesToRead, out bytesRead, advance); /// public ErrorNumber ReadBytesAt(long position, byte[] buffer, int offset, int bytesToRead, out int bytesRead, bool advance = true) { bytesRead = 0; if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return ErrorNumber.NotOpened; } if(position >= _data.Length) { ErrorMessage = Localization.The_requested_position_is_out_of_range; return ErrorNumber.OutOfRange; } if(buffer is null) { ErrorMessage = Localization.Buffer_must_not_be_null; return ErrorNumber.InvalidArgument; } if(offset + bytesToRead > buffer.Length) bytesRead = buffer.Length - offset; if(position + bytesToRead > _data.Length) bytesToRead = (int)(_data.Length - position); Array.Copy(_data, position, buffer, offset, bytesToRead); if(advance) Position = position + bytesToRead; bytesRead = bytesToRead; return ErrorNumber.NoError; } /// public ErrorNumber SetMappings(LinearMemoryMap mappings) { if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return ErrorNumber.NotOpened; } if(!IsWriting) { ErrorMessage = Localization.Image_is_not_opened_for_writing; return ErrorNumber.ReadOnly; } var foundRom = false; var foundChrRom = false; var foundInstRom = false; var foundProm = false; var foundRam = false; var foundChrRam = false; var foundNvram = false; var foundChrNvram = false; var foundMapper = false; var foundSubMapper = false; // Sanitize foreach(LinearMemoryDevice map in mappings.Devices) { Regex regex; Match match; switch(map.Type) { case LinearMemoryType.ROM when !foundRom: _prgLen = (int)map.PhysicalAddress.Length; foundRom = true; break; case LinearMemoryType.CharacterROM when !foundChrRom: _chrLen = (int)map.PhysicalAddress.Length; foundChrRom = true; break; case LinearMemoryType.Trainer when !_trainer: _trainer = true; break; case LinearMemoryType.ROM when !foundInstRom: _instRomLen = (int)map.PhysicalAddress.Length; foundInstRom = true; break; case LinearMemoryType.ROM when !foundProm: _promLen = (int)map.PhysicalAddress.Length; foundProm = true; break; case LinearMemoryType.WorkRAM when !foundRam: _prgRamLen = (int)map.PhysicalAddress.Length; foundRam = true; break; case LinearMemoryType.CharacterRAM when !foundChrRam: _chrRamLen = (int)map.PhysicalAddress.Length; foundChrRam = true; break; case LinearMemoryType.SaveRAM when !foundNvram: _prgNvramLen = (int)map.PhysicalAddress.Length; foundNvram = true; break; case LinearMemoryType.CharacterRAM when !foundChrNvram: _chrNvramLen = (int)map.PhysicalAddress.Length; foundChrNvram = true; break; case LinearMemoryType.Mapper when !foundMapper: regex = MapperRegex(); match = regex.Match(map.Description); if(match.Success) { if(ushort.TryParse(match.Groups["mapper"].Value, out ushort mapper)) { if(_nesHeaderInfo is null) _mapper = mapper; else _nesHeaderInfo.Mapper = mapper; foundMapper = true; } } break; case LinearMemoryType.Mapper when !foundSubMapper: regex = SubmapperRegex(); match = regex.Match(map.Description); if(match.Success) { if(byte.TryParse(match.Groups["mapper"].Value, out byte mapper)) { if(_nesHeaderInfo is null) _submapper = mapper; else _nesHeaderInfo.Submapper = mapper; foundSubMapper = true; } } break; default: return ErrorNumber.InvalidArgument; } } return foundRom && foundMapper ? ErrorNumber.NoError : ErrorNumber.InvalidArgument; } /// public ErrorNumber WriteByte(byte b, bool advance = true) => WriteByteAt(Position, b, advance); /// public ErrorNumber WriteByteAt(long position, byte b, bool advance = true) { if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return ErrorNumber.NotOpened; } if(!IsWriting) { ErrorMessage = Localization.Image_is_not_opened_for_writing; return ErrorNumber.ReadOnly; } if(position >= _data.Length) { ErrorMessage = Localization.The_requested_position_is_out_of_range; return ErrorNumber.OutOfRange; } _data[position] = b; if(advance) Position = position + 1; return ErrorNumber.NoError; } /// public ErrorNumber WriteBytes(byte[] buffer, int offset, int bytesToWrite, out int bytesWritten, bool advance = true) => WriteBytesAt(Position, buffer, offset, bytesToWrite, out bytesWritten, advance); /// public ErrorNumber WriteBytesAt(long position, byte[] buffer, int offset, int bytesToWrite, out int bytesWritten, bool advance = true) { bytesWritten = 0; if(!_opened) { ErrorMessage = Localization.No_image_has_been_opened; return ErrorNumber.NotOpened; } if(!IsWriting) { ErrorMessage = Localization.Image_is_not_opened_for_writing; return ErrorNumber.ReadOnly; } if(position >= _data.Length) { ErrorMessage = Localization.The_requested_position_is_out_of_range; return ErrorNumber.OutOfRange; } if(buffer is null) { ErrorMessage = Localization.Buffer_must_not_be_null; return ErrorNumber.InvalidArgument; } if(offset + bytesToWrite > buffer.Length) bytesToWrite = buffer.Length - offset; if(position + bytesToWrite > _data.Length) bytesToWrite = (int)(_data.Length - position); Array.Copy(buffer, offset, _data, position, bytesToWrite); if(advance) Position = position + bytesToWrite; bytesWritten = bytesToWrite; return ErrorNumber.NoError; } [GeneratedRegex(@"NES Mapper ?(\d+)")] private static partial Regex MapperRegex(); [GeneratedRegex(@"NES Sub-Mapper ?(\d+)")] private static partial Regex SubmapperRegex(); #endregion }