using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using Mesen.Debugger.Utilities; using Mesen.Interop; using Mesen.Utilities; using Mesen.ViewModels; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Mesen.Debugger.ViewModels; public partial class NesHeaderEditViewModel : DisposableViewModel { public NesHeader Header { get; } [ObservableProperty] public partial bool IsBatteryCheckboxEnabled { get; private set; } [ObservableProperty] public partial bool IsVsSystemVisible { get; private set; } [ObservableProperty] public partial bool IsNes20 { get; private set; } [ObservableProperty] public partial Enum[]? AvailableSystemTypes { get; private set; } = null; [ObservableProperty] public partial Enum[]? AvailableTimings { get; private set; } = null; [ObservableProperty] public partial string HeaderBytes { get; private set; } = ""; [ObservableProperty] public partial string ErrorMessage { get; private set; } = ""; private RomInfo _romInfo; private byte[] _prgRom; private byte[] _chrRom; public NesHeaderEditViewModel() { bool releaseDebugger = !DebugWindowManager.HasOpenedDebugWindows(); bool paused = EmuApi.IsPaused(); byte[] headerBytes = DebugApi.GetRomHeader(); _prgRom = DebugApi.GetMemoryState(MemoryType.NesPrgRom); _chrRom = DebugApi.GetMemoryState(MemoryType.NesChrRom); if(releaseDebugger) { //GetRomHeader will initialize the debugger - stop the debugger if no other debug window is opened DebugApi.ReleaseDebugger(); if(paused) { EmuApi.Pause(); } } if(headerBytes.Length < 16) { Array.Resize(ref headerBytes, 16); } _romInfo = EmuApi.GetRomInfo(); Header = NesHeader.FromBytes(headerBytes); AddDisposable(Header.ObserveProp([nameof(Header.SaveRam), nameof(Header.ChrRamBattery)], () => { IsBatteryCheckboxEnabled = Header.SaveRam == MemorySizes.None && Header.ChrRamBattery == MemorySizes.None; if(!IsBatteryCheckboxEnabled) { Header.HasBattery = true; } })); AddDisposable(Header.ObserveProp([nameof(Header.System), nameof(Header.FileType)], () => { bool isVsSystem = Header.System == TvSystem.VsSystem; IsVsSystemVisible = isVsSystem && Header.FileType == NesFileType.Nes2_0; if(!IsVsSystemVisible) { Header.VsPpu = VsPpuType.RP2C03B; Header.VsSystem = VsSystemType.Default; } })); AddDisposable(Header.ObserveProp(nameof(Header.FileType), () => { IsNes20 = Header.FileType == NesFileType.Nes2_0; if(IsNes20) { AvailableSystemTypes = null; AvailableTimings = null; } else { AvailableSystemTypes = new Enum[] { TvSystem.NesFamicomDendy, TvSystem.VsSystem, TvSystem.Playchoice }; AvailableTimings = new Enum[] { FrameTiming.Ntsc, FrameTiming.Pal, FrameTiming.NtscAndPal }; Header.SubmapperId = 0; Header.ChrRam = MemorySizes.None; Header.ChrRamBattery = MemorySizes.None; Header.SaveRam = MemorySizes.None; Header.WorkRam = MemorySizes.None; } })); Header.PropertyChanged += Header_PropertyChanged; UpdateHeaderPreview(); } protected override void DisposeView() { base.DisposeView(); Header.PropertyChanged -= Header_PropertyChanged; } private void Header_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { UpdateHeaderPreview(); ErrorMessage = GetErrorMessage(); } private void UpdateHeaderPreview() { byte[] headerBytes = Header.ToBytes(); StringBuilder sb = new(); for(int i = 0; i < 16; i++) { sb.Append(headerBytes[i].ToString("X2") + " "); } HeaderBytes = sb.ToString().Trim(); } private string GetErrorMessage() { if(Header.PrgRom <= 0) { return "Error: PRG ROM size cannot be 0"; } if(Header.FileType == NesFileType.Nes2_0) { bool isValidPrgSize = NesHeader.IsValidSize(Header.PrgRom); bool isValidChrSize = NesHeader.IsValidSize(Header.ChrRom); if(Header.MapperId >= 4096) { return "Error: Mapper ID must be lower than 4096"; } else if(Header.SubmapperId >= 16) { return "Error: Submapper ID must be lower than 16"; } else if(!isValidPrgSize && (Header.GetPrgSize() > 61424 * 1024 || (Header.GetPrgSize() % (16 * 1024)) != 0)) { return "Error: PRG ROM must be <= 61424 KB and a multiple of 16 KB (or a valid exponent-multiplier value)"; } else if(!isValidChrSize && (Header.GetChrSize() > 30712 * 1024 || (Header.GetChrSize() % (8 * 1024)) != 0)) { return "Error: CHR ROM must be <= 30712 KB and a multiple of 8 KB (or a valid exponent-multiplier value)"; } } else { if(Header.MapperId >= 256) { return "Error: Mapper ID must be lower than 256"; } else if(Header.PrgRom > 4096) { return "Error: PRG ROM size must be lower than or equal to 4096 KB"; } else if(Header.ChrRom > 2040) { return "Error: CHR ROM size must be lower than or equal to 2040 KB"; } else if((Header.PrgRom % 16) != 0) { return "Error: PRG ROM size must be a multiple of 16 KB"; } else if((Header.ChrRom % 8) != 0) { return "Error: CHR ROM size must be a multiple of 8 KB"; } } return ""; } public async Task Save(Window wnd) { string? filepath = await FileDialogHelper.SaveFile(Path.GetDirectoryName(_romInfo.RomPath), _romInfo.GetRomName(), wnd, FileDialogHelper.NesExt); if(filepath != null) { byte[] data = new byte[_prgRom.Length + _chrRom.Length + 16]; byte[] header = Header.ToBytes(); Array.Copy(header, data, 16); Array.Copy(_prgRom, 0, data, 16, _prgRom.Length); Array.Copy(_chrRom, 0, data, 16 + _prgRom.Length, _chrRom.Length); return FileHelper.WriteAllBytes(filepath, data); } return false; } public enum RomSizeUnit { Bytes, KB, MB } public partial class NesHeader : ViewModelBase { private static Dictionary _validSizeValues = new Dictionary(); [ObservableProperty] public partial NesFileType FileType { get; set; } [ObservableProperty] public partial uint MapperId { get; set; } [ObservableProperty] public partial uint SubmapperId { get; set; } [ObservableProperty] public partial UInt64 PrgRom { get; set; } [ObservableProperty] public partial RomSizeUnit PrgRomUnit { get; set; } [ObservableProperty] public partial UInt64 ChrRom { get; set; } [ObservableProperty] public partial RomSizeUnit ChrRomUnit { get; set; } [ObservableProperty] public partial MirroringType Mirroring { get; set; } [ObservableProperty] public partial FrameTiming Timing { get; set; } [ObservableProperty] public partial TvSystem System { get; set; } [ObservableProperty] public partial bool HasTrainer { get; set; } [ObservableProperty] public partial bool HasBattery { get; set; } [ObservableProperty] public partial VsPpuType VsPpu { get; set; } [ObservableProperty] public partial VsSystemType VsSystem { get; set; } [ObservableProperty] public partial GameInputType InputType { get; set; } [ObservableProperty] public partial MemorySizes WorkRam { get; set; } = MemorySizes.None; [ObservableProperty] public partial MemorySizes SaveRam { get; set; } = MemorySizes.None; [ObservableProperty] public partial MemorySizes ChrRam { get; set; } = MemorySizes.None; [ObservableProperty] public partial MemorySizes ChrRamBattery { get; set; } = MemorySizes.None; static NesHeader() { _validSizeValues = new Dictionary(); for(int i = 0; i < 256; i++) { int multiplier = (i & 0x03) * 2 + 1; UInt64 value = ((UInt64)1 << (i >> 2)); _validSizeValues[(UInt64)multiplier * value] = i; } } public byte[] ToBytes() { byte[] header = new byte[16]; header[0] = 0x4E; header[1] = 0x45; header[2] = 0x53; header[3] = 0x1A; UInt64 prgRomSize = GetPrgSize(); UInt64 chrRomSize = GetChrSize(); UInt64 prgRomValue = prgRomSize / (16 * 1024); UInt64 chrRomValue = chrRomSize / (8 * 1024); if(FileType == NesFileType.Nes2_0) { if((prgRomSize % 16384) != 0 || prgRomSize > 61424 * 1024) { if(_validSizeValues.ContainsKey(prgRomSize)) { //This value is a valid exponent+multiplier combo (NES 2.0 only) prgRomValue = ((uint)_validSizeValues[prgRomSize] & 0xFF) | 0xF00; } } if((chrRomSize % 8192) != 0 || chrRomSize > 30712 * 1024) { if(_validSizeValues.ContainsKey(chrRomSize)) { //This value is a valid exponent+multiplier combo (NES 2.0 only) chrRomValue = ((uint)_validSizeValues[chrRomSize] & 0xFF) | 0xF00; } } //NES 2.0 header[4] = (byte)(prgRomValue); header[5] = (byte)(chrRomValue); header[6] = (byte)( ((byte)(MapperId & 0x0F) << 4) | (byte)Mirroring | (HasTrainer ? 0x04 : 0x00) | (HasBattery ? 0x02 : 0x00) ); header[7] = (byte)(MapperId & 0xF0); switch(System) { case TvSystem.NesFamicomDendy: header[7] |= 0x00; break; case TvSystem.VsSystem: header[7] |= 0x01; break; case TvSystem.Playchoice: header[7] |= 0x02; break; default: header[7] |= 0x03; break; } //Enable NES 2.0 header header[7] |= 0x08; header[8] = (byte)(((SubmapperId & 0x0F) << 4) | ((MapperId & 0xF00) >> 8)); header[9] = (byte)(((prgRomValue & 0xF00) >> 8) | ((chrRomValue & 0xF00) >> 4)); header[10] = (byte)((byte)WorkRam | ((byte)SaveRam) << 4); header[11] = (byte)((byte)ChrRam | ((byte)ChrRamBattery) << 4); header[12] = (byte)Timing; if(System == TvSystem.VsSystem) { header[13] = (byte)(((byte)VsPpu & 0x0F) | ((((byte)VsSystem) & 0x0F) << 4)); } else { header[13] = (byte)System; } header[14] = 0; header[15] = (byte)InputType; } else { //iNES if(prgRomValue == 0x100) { header[4] = 0; } else { header[4] = (byte)(prgRomValue); } header[5] = (byte)(chrRomValue); header[6] = (byte)( ((byte)(MapperId & 0x0F) << 4) | (byte)Mirroring | (HasTrainer ? 0x04 : 0x00) | (HasBattery ? 0x02 : 0x00) ); header[7] = (byte)( ((byte)MapperId & 0xF0) | (byte)(System == TvSystem.VsSystem ? 0x01 : 0x00) | (System == TvSystem.Playchoice ? 0x02 : 0x00) ); header[8] = 0; header[9] = (byte)(Timing == FrameTiming.Pal ? 0x01 : 0x00); header[10] = 0; header[11] = 0; header[12] = 0; header[13] = 0; header[14] = 0; header[15] = 0; } return header; } public static NesHeader FromBytes(byte[] bytes) { BinaryHeader binHeader = new BinaryHeader(bytes); NesHeader header = new NesHeader(); header.FileType = binHeader.GetRomHeaderVersion() == RomHeaderVersion.Nes2_0 ? NesFileType.Nes2_0 : NesFileType.iNes; header.PrgRom = (uint)(binHeader.GetPrgSize()); if(header.PrgRom > 0 && header.PrgRom % 1024 == 0) { header.PrgRomUnit = RomSizeUnit.KB; header.PrgRom /= 1024; if(header.PrgRom % 1024 == 0) { header.PrgRomUnit = RomSizeUnit.MB; header.PrgRom /= 1024; } } header.ChrRom = (uint)(binHeader.GetChrSize()); if(header.ChrRom > 0 && header.ChrRom % 1024 == 0) { header.ChrRomUnit = RomSizeUnit.KB; header.ChrRom /= 1024; if(header.ChrRom % 1024 == 0) { header.ChrRomUnit = RomSizeUnit.MB; header.ChrRom /= 1024; } } header.HasTrainer = binHeader.HasTrainer(); header.HasBattery = binHeader.HasBattery(); header.System = binHeader.GetTvSystem(); header.Timing = binHeader.GetFrameTiming(); header.Mirroring = binHeader.GetMirroringType(); header.MapperId = (uint)binHeader.GetMapperID(); header.SubmapperId = (uint)binHeader.GetSubMapper(); header.WorkRam = (MemorySizes)binHeader.GetWorkRamSize(); header.SaveRam = (MemorySizes)binHeader.GetSaveRamSize(); header.ChrRam = (MemorySizes)binHeader.GetChrRamSize(); header.ChrRamBattery = (MemorySizes)binHeader.GetSaveChrRamSize(); header.InputType = binHeader.GetInputType(); header.VsPpu = (VsPpuType)(bytes[13] & 0x0F); header.VsSystem = binHeader.GetVsSystemType(); return header; } public static bool IsValidSize(ulong size) { return _validSizeValues.ContainsKey(size); } private UInt64 GetSize(UInt64 size, RomSizeUnit unit) { switch(unit) { case RomSizeUnit.Bytes: return size; case RomSizeUnit.KB: return size * 1024; case RomSizeUnit.MB: return size * 1024 * 1024; default: throw new Exception("Unsupported value"); } } public UInt64 GetPrgSize() { return GetSize(PrgRom, PrgRomUnit); } public UInt64 GetChrSize() { return GetSize(ChrRom, ChrRomUnit); } } private class BinaryHeader { private byte[] _bytes; private byte _prgCount; private byte _chrCount; public BinaryHeader(byte[] bytes) { _bytes = bytes; _prgCount = bytes[4]; _chrCount = bytes[5]; } public RomHeaderVersion GetRomHeaderVersion() { if((_bytes[7] & 0x0C) == 0x08) { return RomHeaderVersion.Nes2_0; } else if((_bytes[7] & 0x0C) == 0x00) { return RomHeaderVersion.iNes; } else { return RomHeaderVersion.OldiNes; } } public int GetMapperID() { switch(GetRomHeaderVersion()) { case RomHeaderVersion.Nes2_0: return ((_bytes[8] & 0x0F) << 8) | (_bytes[7] & 0xF0) | (_bytes[6] >> 4); default: case RomHeaderVersion.iNes: return (_bytes[7] & 0xF0) | (_bytes[6] >> 4); case RomHeaderVersion.OldiNes: return (_bytes[6] >> 4); } } public bool HasBattery() { return (_bytes[6] & 0x02) == 0x02; } public bool HasTrainer() { return (_bytes[6] & 0x04) == 0x04; } public FrameTiming GetFrameTiming() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { return (FrameTiming)(_bytes[12] & 0x03); } else if(GetRomHeaderVersion() == RomHeaderVersion.iNes) { return (_bytes[9] & 0x01) == 0x01 ? FrameTiming.Pal : FrameTiming.Ntsc; } return FrameTiming.Ntsc; } public TvSystem GetTvSystem() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { switch(_bytes[7] & 0x03) { case 0: return TvSystem.NesFamicomDendy; case 1: return TvSystem.VsSystem; case 2: return TvSystem.Playchoice; case 3: return (TvSystem)_bytes[13]; } } else if(GetRomHeaderVersion() == RomHeaderVersion.iNes) { if((_bytes[7] & 0x01) == 0x01) { return TvSystem.VsSystem; } else if((_bytes[7] & 0x02) == 0x02) { return TvSystem.Playchoice; } } return TvSystem.NesFamicomDendy; } private UInt64 GetSizeValue(int exponent, int multiplier) { multiplier = multiplier * 2 + 1; return (UInt64)multiplier * ((UInt64)1 << exponent); } public UInt64 GetPrgSize() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { if((_bytes[9] & 0x0F) == 0x0F) { return GetSizeValue(_prgCount >> 2, _prgCount & 0x03); } else { return (UInt64)(((_bytes[9] & 0x0F) << 8) | _prgCount) * 16 * 1024; } } else { if(_prgCount == 0) { return 256 * 16 * 1024; //0 is a special value and means 256 } else { return (UInt64)_prgCount * 16 * 1024; } } } public UInt64 GetChrSize() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { if((_bytes[9] & 0xF0) == 0xF0) { return GetSizeValue(_chrCount >> 2, _chrCount & 0x03); } else { return (UInt64)(((_bytes[9] & 0xF0) << 4) | _chrCount) * 8 * 1024; } } else { return (UInt64)_chrCount * 8 * 1024; } } public int GetWorkRamSize() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { return _bytes[10] & 0x0F; } else { return 0; } } public int GetSaveRamSize() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { return (_bytes[10] & 0xF0) >> 4; } else { return 0; } } public int GetChrRamSize() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { return _bytes[11] & 0x0F; } else { return 0; } } public int GetSaveChrRamSize() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { return (_bytes[11] & 0xF0) >> 4; } else { return 0; } } public int GetSubMapper() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { return (_bytes[8] & 0xF0) >> 4; } else { return 0; } } public MirroringType GetMirroringType() { if((_bytes[6] & 0x08) != 0) { return MirroringType.FourScreens; } else { return (_bytes[6] & 0x01) != 0 ? MirroringType.Vertical : MirroringType.Horizontal; } } public GameInputType GetInputType() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { if(_bytes[15] < Enum.GetValues().Length) { return (GameInputType)_bytes[15]; } return GameInputType.Unspecified; } else { return GameInputType.Unspecified; } } public VsSystemType GetVsSystemType() { if(GetRomHeaderVersion() == RomHeaderVersion.Nes2_0) { if((_bytes[13] >> 4) <= 0x06) { return (VsSystemType)(_bytes[13] >> 4); } } return VsSystemType.Default; } } public enum RomHeaderVersion { iNes = 0, Nes2_0 = 1, OldiNes = 2 } public enum NesFileType { iNes = 0, Nes2_0 = 1 } public enum MirroringType { Horizontal = 0, Vertical = 1, FourScreens = 8 } public enum FrameTiming { Ntsc = 0, Pal = 1, NtscAndPal = 2, Dendy = 3 } public enum TvSystem { NesFamicomDendy, VsSystem, Playchoice, CloneWithDecimal, EpsmModule, Vt01RedCyan, Vt02, Vt03, Vt09, Vt32, Vt369, UmcUm6578, FamicomNetworkSystem, ReservedD, ReservedE, ReservedF } public enum MemorySizes { None = 0, _128Bytes = 1, _256Bytes = 2, _512Bytes = 3, _1KB = 4, _2KB = 5, _4KB = 6, _8KB = 7, _16KB = 8, _32KB = 9, _64KB = 10, _128KB = 11, _256KB = 12, _512KB = 13, _1024KB = 14, Reserved = 15 } public enum VsPpuType { RP2C03B = 0, RP2C03G = 1, RP2C040001 = 2, RP2C040002 = 3, RP2C040003 = 4, RP2C040004 = 5, RC2C03B = 6, RC2C03C = 7, RC2C0501 = 8, RC2C0502 = 9, RC2C0503 = 10, RC2C0504 = 11, RC2C0505 = 12, ReservedD = 13, ReservedE = 14, ReservedF = 15 } public enum VsSystemType { Default = 0, RbiBaseballProtection = 1, TkoBoxingProtection = 2, SuperXeviousProtection = 3, IceClimberProtection = 4, VsDualSystem = 5, RaidOnBungelingBayProtection = 6, } public enum GameInputType { Unspecified = 0, StandardControllers = 1, FourScore = 2, FourPlayerAdapter = 3, VsSystem = 4, VsSystemSwapped = 5, VsSystemSwapAB = 6, VsZapper = 7, Zapper = 8, TwoZappers = 9, BandaiHypershot = 0x0A, PowerPadSideA = 0x0B, PowerPadSideB = 0x0C, FamilyTrainerSideA = 0x0D, FamilyTrainerSideB = 0x0E, ArkanoidControllerNes = 0x0F, ArkanoidControllerFamicom = 0x10, DoubleArkanoidController = 0x11, KonamiHyperShot = 0x12, PachinkoController = 0x13, ExcitingBoxing = 0x14, JissenMahjong = 0x15, PartyTap = 0x16, OekaKidsTablet = 0x17, BarcodeBattler = 0x18, MiraclePiano = 0x19, //not supported yet PokkunMoguraa = 0x1A, //not supported yet TopRider = 0x1B, //not supported yet DoubleFisted = 0x1C, //not supported yet Famicom3dSystem = 0x1D, //not supported yet DoremikkoKeyboard = 0x1E, //not supported yet ROB = 0x1F, //not supported yet FamicomDataRecorder = 0x20, TurboFile = 0x21, BattleBox = 0x22, FamilyBasicKeyboard = 0x23, Pec586Keyboard = 0x24, //not supported yet Bit79Keyboard = 0x25, //not supported yet SuborKeyboard = 0x26, SuborKeyboardMouse1 = 0x27, SuborKeyboardMouse2 = 0x28, SnesMouse = 0x29, GenericMulticart = 0x2A, //not supported yet SnesControllers = 0x2B, RacerMateBicycle = 0x2C, //not supported yet UForce = 0x2D, //not supported yet RobStackUp = 0x2E, //not supported yet CityPatrolmanLightgun = 0x2F, //not supported yet SharpC1CassetteInterface = 0x30, //not supported yet StandardControllerWithSwappedButtons = 0x31, //not supported yet ExcaliburSudokuPad = 0x32, //not supported yet AblPinball = 0x33, //not supported yet GoldenNuggetCasino = 0x34, //not supported yet KedaKeyboard = 0x35, //not supported yet SuborKeyboardMouse3 = 0x36, //not supported yet (note: mouse on $4017) PortTestController = 0x37, //not supported yet BandaiMultiGamePlayer = 0x38, //not supported yet VenomTVDance = 0x39, //not supported yet LgTvRemote = 0x3A, //not supported yet FcnsController = 0x3B } }