Files
MesenCE/UI/Debugger/ViewModels/NesHeaderEditViewModel.cs
SourMesen 40335ce735 UI: Update to Avalonia 12, .NET 10 and replace ReactiveUI with MVVM community toolkit (#142)
Also fixes DX11.0 support (for Win7 PCs that aren't fully updated), which stopped working after the recent variable refresh rate changes
2026-07-03 16:04:42 +09:00

729 lines
20 KiB
C#

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<bool> 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<UInt64, int> _validSizeValues = new Dictionary<UInt64, int>();
[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<UInt64, int>();
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<GameInputType>().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
}
}