Emulator/GbaSavedata.cs
using System.IO;
using System.Text;

namespace sGBA;

public class GbaSavedata
{
	public Gba Gba { get; }
	public SavedataType Type { get; private set; }
	public byte[] Data { get; set; }
	public SavedataCommand Command { get; set; }
	public FlashStateMachine FlashState { get; set; }

	public int ReadBitsRemaining { get; set; }
	public uint ReadAddress { get; set; }
	public uint WriteAddress { get; set; }

	public uint Settling { get; set; }
	public long DustEndCycle { get; set; }

	public int Dirty { get; set; }
	public uint DirtAge { get; set; }

	private int _currentBank;

	private const int FlashEraseCycles = 30000;
	private const int FlashProgramCycles = 650;
	private const int EepromSettleCycles = 115000;
	private const int SavedataCleanupThreshold = 15;

	private const int DirtNone = 0;
	private const int DirtNew = 1;
	private const int DirtSeen = 2;

	private const int FlashBaseHi = 0x5555;
	private const int FlashBaseLo = 0x2AAA;

	private const ushort FlashPanasonicMn63F805Mnp = 0x1B32;
	private const ushort FlashSanyoLe26Fv10N1Ts = 0x1362;

	public GbaSavedata( Gba gba )
	{
		Gba = gba;
		Type = SavedataType.None;
		Data = [];
		Command = SavedataCommand.EepromNull;
		FlashState = FlashStateMachine.Raw;
	}

	public void Reset()
	{
		Command = SavedataCommand.EepromNull;
		FlashState = FlashStateMachine.Raw;
		ReadBitsRemaining = 0;
		ReadAddress = 0;
		WriteAddress = 0;
		_currentBank = 0;
		Settling = 0;
		DustEndCycle = 0;
		Dirty = DirtNone;
		DirtAge = 0;
	}

	public void ForceType( byte[] rom )
	{
		string romText = Encoding.ASCII.GetString( rom );

		if ( romText.Contains( "FLASH1M_V" ) )
		{
			Type = SavedataType.Flash1M;
			Data = new byte[GbaConstants.Flash1MSize];
			Array.Fill( Data, (byte)0xFF );
			_currentBank = 0;
			GbaLog.Write( LogCategory.GBASave, LogLevel.Info, "Detected Flash 128K savegame" );
		}
		else if ( romText.Contains( "FLASH_V" ) || romText.Contains( "FLASH512_V" ) )
		{
			Type = SavedataType.Flash512;
			Data = new byte[GbaConstants.Flash512Size];
			Array.Fill( Data, (byte)0xFF );
			_currentBank = 0;
			GbaLog.Write( LogCategory.GBASave, LogLevel.Info, "Detected Flash 64K savegame" );
		}
		else if ( romText.Contains( "SRAM_V" ) || romText.Contains( "SRAM_F_V" ) )
		{
			Type = SavedataType.Sram;
			Data = new byte[GbaConstants.SramSize];
			Array.Fill( Data, (byte)0xFF );
			GbaLog.Write( LogCategory.GBASave, LogLevel.Info, "Detected SRAM savegame" );
		}
		else if ( romText.Contains( "EEPROM_V" ) )
		{
			Type = SavedataType.Eeprom;
			Data = new byte[GbaConstants.EepromSize];
			Array.Fill( Data, (byte)0xFF );
			GbaLog.Write( LogCategory.GBASave, LogLevel.Info, "Detected EEPROM savegame" );
		}
		else
		{
			Type = SavedataType.None;
			Data = [];
		}
	}

	public void Load( byte[] saveData )
	{
		if ( saveData == null || saveData.Length == 0 || Data.Length == 0 )
			return;

		int copyLen = Math.Min( saveData.Length, Data.Length );
		Array.Copy( saveData, Data, copyLen );
	}

	private bool IsDustScheduled()
	{
		return DustEndCycle > Gba.Cpu.Cycles;
	}

	private void ScheduleDust( int cycles )
	{
		DustEndCycle = Gba.Cpu.Cycles + cycles;
	}

	public byte Read8( uint address )
	{
		uint offset = address & 0xFFFF;

		switch ( Type )
		{
			case SavedataType.Sram:
				return Data[offset & (uint)(Data.Length - 1)];

			case SavedataType.Flash512:
			case SavedataType.Flash1M:
				return ReadFlash( (ushort)offset );

			default:
				return 0;
		}
	}

	public void Write8( uint address, byte value )
	{
		uint offset = address & 0xFFFF;

		switch ( Type )
		{
			case SavedataType.Sram:
				Data[offset & (uint)(Data.Length - 1)] = value;
				Dirty |= DirtNew;
				break;

			case SavedataType.Flash512:
			case SavedataType.Flash1M:
				WriteFlash( (ushort)offset, value );
				break;
		}
	}

	private byte ReadFlash( ushort address )
	{
		if ( Command == SavedataCommand.FlashId )
		{
			if ( Type == SavedataType.Flash512 )
			{
				if ( address < 2 )
					return (byte)(FlashPanasonicMn63F805Mnp >> (address * 8));
			}
			else if ( Type == SavedataType.Flash1M )
			{
				if ( address < 2 )
					return (byte)(FlashSanyoLe26Fv10N1Ts >> (address * 8));
			}
		}

		if ( IsDustScheduled() && (address >> 12) == Settling )
			return (byte)((Data[_currentBank + address] ^ 0x80) & 0x80);

		return Data[_currentBank + address];
	}

	private void WriteFlash( ushort address, byte value )
	{
		switch ( FlashState )
		{
			case FlashStateMachine.Raw:
				switch ( Command )
				{
					case SavedataCommand.FlashProgram:
						Dirty |= DirtNew;
						Data[_currentBank + address] = value;
						Command = SavedataCommand.FlashNone;
						ScheduleDust( FlashProgramCycles );
						break;

					case SavedataCommand.FlashSwitchBank:
						if ( address == 0 && value < 2 )
							FlashSwitchBank( value );
						Command = SavedataCommand.FlashNone;
						break;

					default:
						if ( address == FlashBaseHi && value == (byte)SavedataCommand.FlashStart )
							FlashState = FlashStateMachine.Start;
						break;
				}
				break;

			case FlashStateMachine.Start:
				if ( address == FlashBaseLo && value == (byte)SavedataCommand.FlashContinue )
					FlashState = FlashStateMachine.Continue;
				else
					FlashState = FlashStateMachine.Raw;
				break;

			case FlashStateMachine.Continue:
				FlashState = FlashStateMachine.Raw;
				if ( address == FlashBaseHi )
				{
					switch ( Command )
					{
						case SavedataCommand.FlashNone:
							switch ( (SavedataCommand)value )
							{
								case SavedataCommand.FlashErase:
								case SavedataCommand.FlashId:
								case SavedataCommand.FlashProgram:
								case SavedataCommand.FlashSwitchBank:
									Command = (SavedataCommand)value;
									break;
							}
							break;

						case SavedataCommand.FlashErase:
							if ( value == (byte)SavedataCommand.FlashEraseChip )
								FlashErase();
							Command = SavedataCommand.FlashNone;
							break;

						case SavedataCommand.FlashId:
							if ( value == (byte)SavedataCommand.FlashTerminate )
								Command = SavedataCommand.FlashNone;
							break;

						default:
							Command = SavedataCommand.FlashNone;
							break;
					}
				}
				else if ( Command == SavedataCommand.FlashErase )
				{
					if ( value == (byte)SavedataCommand.FlashEraseSector )
					{
						FlashEraseSector( address );
						Command = SavedataCommand.FlashNone;
					}
				}
				break;
		}
	}

	private void FlashSwitchBank( int bank )
	{
		if ( bank > 0 && Type == SavedataType.Flash512 )
		{
			GbaLog.Write( LogCategory.GBASave, LogLevel.Info, "Upgrading flash chip from 512kb to 1Mb" );
			Type = SavedataType.Flash1M;
			byte[] newData = new byte[GbaConstants.Flash1MSize];
			Array.Fill( newData, (byte)0xFF );
			Array.Copy( Data, newData, Data.Length );
			Data = newData;
		}
		_currentBank = bank << 16;
	}

	private void FlashErase()
	{
		Dirty |= DirtNew;
		int size = Type == SavedataType.Flash1M ? GbaConstants.Flash1MSize : GbaConstants.Flash512Size;
		Array.Fill( Data, (byte)0xFF, 0, size );
	}

	private void FlashEraseSector( ushort sectorStart )
	{
		Dirty |= DirtNew;
		Settling = (uint)(sectorStart >> 12);
		ScheduleDust( FlashEraseCycles );
		Array.Fill( Data, (byte)0xFF, _currentBank + (sectorStart & 0xF000), 0x1000 );
	}

	public ushort ReadEEPROM()
	{
		if ( Command != SavedataCommand.EepromRead )
		{
			if ( !IsDustScheduled() )
				return 1;
			return 0;
		}

		--ReadBitsRemaining;
		if ( ReadBitsRemaining < 64 )
		{
			int step = 63 - ReadBitsRemaining;
			uint address = (ReadAddress + (uint)step) >> 3;
			if ( address >= GbaConstants.EepromSize )
			{
				if ( ReadBitsRemaining == 0 )
					Command = SavedataCommand.EepromNull;
				return 0xFF;
			}
			byte data = Data[address];
			if ( ReadBitsRemaining == 0 )
				Command = SavedataCommand.EepromNull;
			return (ushort)((data >> (0x7 - (step & 0x7))) & 0x1);
		}
		return 0;
	}

	public void WriteEEPROM( ushort value, int writeSize )
	{
		switch ( Command )
		{
			case SavedataCommand.EepromNull:
			default:
				Command = (SavedataCommand)(value & 0x1);
				break;

			case SavedataCommand.EepromPending:
				Command = (SavedataCommand)(((int)Command << 1) | (value & 0x1));
				if ( Command == SavedataCommand.EepromWrite )
					WriteAddress = 0;
				else
					ReadAddress = 0;
				break;

			case SavedataCommand.EepromWrite:
				if ( writeSize > 65 )
				{
					WriteAddress <<= 1;
					WriteAddress |= (uint)(value & 0x1) << 6;
				}
				else if ( writeSize == 1 )
				{
					Command = SavedataCommand.EepromNull;
				}
				else
				{
					uint byteAddr = WriteAddress >> 3;
					if ( byteAddr < GbaConstants.EepromSize )
					{
						Dirty |= DirtNew;
						byte current = Data[byteAddr];
						current &= (byte)~(1 << (0x7 - (int)(WriteAddress & 0x7)));
						current |= (byte)((value & 0x1) << (0x7 - (int)(WriteAddress & 0x7)));
						Data[byteAddr] = current;
						ScheduleDust( EepromSettleCycles );
					}
					++WriteAddress;
				}
				break;

			case SavedataCommand.EepromReadPending:
				if ( writeSize > 1 )
				{
					ReadAddress <<= 1;
					if ( (value & 0x1) != 0 )
						ReadAddress |= 0x40;
				}
				else
				{
					ReadBitsRemaining = 68;
					Command = SavedataCommand.EepromRead;
				}
				break;
		}
	}

	public bool Clean()
	{
		if ( (Dirty & DirtNew) != 0 )
		{
			DirtAge = 0;
			Dirty &= ~DirtNew;
			Dirty |= DirtSeen;
		}
		else if ( (Dirty & DirtSeen) != 0 )
		{
			++DirtAge;
			if ( DirtAge > SavedataCleanupThreshold )
			{
				Dirty = DirtNone;
				return true;
			}
		}
		return false;
	}

	public void Serialize( BinaryWriter w )
	{
		w.Write( (int)Command );
		w.Write( (int)FlashState );
		w.Write( _currentBank );
		w.Write( ReadBitsRemaining );
		w.Write( ReadAddress );
		w.Write( WriteAddress );
		w.Write( Settling );
		w.Write( DustEndCycle );
		w.Write( Dirty );
		w.Write( DirtAge );
	}

	public void Deserialize( BinaryReader r )
	{
		Command = (SavedataCommand)r.ReadInt32();
		FlashState = (FlashStateMachine)r.ReadInt32();
		_currentBank = r.ReadInt32();
		ReadBitsRemaining = r.ReadInt32();
		ReadAddress = r.ReadUInt32();
		WriteAddress = r.ReadUInt32();
		Settling = r.ReadUInt32();
		DustEndCycle = r.ReadInt64();
		Dirty = r.ReadInt32();
		DirtAge = r.ReadUInt32();
	}
}

public enum SavedataType
{
	None = 0,
	Sram = 1,
	Flash512 = 2,
	Flash1M = 3,
	Eeprom = 4,
}

public enum SavedataCommand
{
	EepromNull = 0,
	EepromPending = 1,
	EepromWrite = 2,
	EepromReadPending = 3,
	EepromRead = 4,

	FlashStart = 0xAA,
	FlashContinue = 0x55,

	FlashEraseChip = 0x10,
	FlashEraseSector = 0x30,

	FlashNone = 0,
	FlashErase = 0x80,
	FlashId = 0x90,
	FlashProgram = 0xA0,
	FlashSwitchBank = 0xB0,
	FlashTerminate = 0xF0,
}

public enum FlashStateMachine
{
	Raw = 0,
	Start = 1,
	Continue = 2,
}