Emulator/GbaCartridgeHardware.cs
using System.IO;

namespace sGBA;

public class GbaCartridgeHardware
{
	public Gba Gba { get; }
	public bool HasRtc { get; set; }

	private byte _writeLatch;
	private byte _pinState;
	private byte _direction;
	private bool _readWrite;

	private int _rtcBytesRemaining;
	private int _rtcBitsRead;
	private int _rtcBits;
	private bool _rtcCommandActive;
	private bool _rtcSckEdge;
	private bool _rtcSioOutput;
	private int _rtcCommand;
	private byte _rtcControl;
	private byte[] _rtcTime = new byte[7];

	private static readonly int[] RtcBytes = [0, 0, 7, 0, 1, 0, 3, 0];

	public GbaCartridgeHardware( Gba gba )
	{
		Gba = gba;
	}

	public void Reset()
	{
		_writeLatch = 0;
		_pinState = 0;
		_direction = 0;
		_readWrite = false;

		_rtcBytesRemaining = 0;
		_rtcBitsRead = 0;
		_rtcBits = 0;
		_rtcCommandActive = false;
		_rtcSckEdge = true;
		_rtcSioOutput = true;
		_rtcCommand = 0;
		_rtcControl = 0x40;
		Array.Clear( _rtcTime );
	}

	public void GpioWrite( uint address, ushort value )
	{
		uint reg = address & 0xFF;

		switch ( reg )
		{
			case 0xC4:
				_writeLatch = (byte)(value & 0xF);
				_pinState &= (byte)~_direction;
				_pinState |= (byte)(_writeLatch & _direction);
				ReadPins();
				break;

			case 0xC6:
				_direction = (byte)(value & 0xF);
				_pinState &= (byte)~_direction;
				_pinState |= (byte)(_writeLatch & _direction);
				ReadPins();
				break;

			case 0xC8:
				_readWrite = (value & 1) != 0;
				break;
		}

		UpdateRomMirror();
	}

	public ushort GpioRead( uint address )
	{
		if ( !_readWrite )
			return 0;

		uint reg = address & 0xFF;
		return reg switch
		{
			0xC4 => _pinState,
			0xC6 => _direction,
			0xC8 => (ushort)(_readWrite ? 1 : 0),
			_ => 0
		};
	}

	private void UpdateRomMirror()
	{
		var rom = Gba.Memory.Rom;
		if ( rom.Length < 0xCA )
			return;

		if ( _readWrite )
		{
			rom[0xC4] = _pinState;
			rom[0xC5] = 0;
			rom[0xC6] = _direction;
			rom[0xC7] = 0;
			rom[0xC8] = (byte)(_readWrite ? 1 : 0);
			rom[0xC9] = 0;
		}
		else
		{
			rom[0xC4] = 0;
			rom[0xC5] = 0;
			rom[0xC6] = 0;
			rom[0xC7] = 0;
			rom[0xC8] = 0;
			rom[0xC9] = 0;
		}
	}

	private void ReadPins()
	{
		if ( HasRtc )
			RtcReadPins();
	}

	private void OutputPins( int pins )
	{
		_pinState &= _direction;
		_pinState |= (byte)(pins & ~_direction & 0xF);

		if ( _readWrite )
		{
			var rom = Gba.Memory.Rom;
			if ( rom.Length > 0xC5 )
			{
				rom[0xC4] = _pinState;
				rom[0xC5] = 0;
			}
		}
	}
	private void RtcReadPins()
	{
		OutputPins( _pinState & 2 );

		if ( (_pinState & 4) == 0 )
		{
			_rtcBitsRead = 0;
			_rtcBytesRemaining = 0;
			_rtcCommandActive = false;
			_rtcCommand = 0;
			_rtcSckEdge = true;
			_rtcSioOutput = true;
			OutputPins( 2 );
			return;
		}

		if ( !_rtcCommandActive )
		{
			OutputPins( 2 );

			if ( (_pinState & 1) == 0 )
			{
				_rtcBits &= ~(1 << _rtcBitsRead);
				_rtcBits |= ((_pinState >> 1) & 1) << _rtcBitsRead;
			}

			if ( !_rtcSckEdge && (_pinState & 1) != 0 )
			{
				_rtcBitsRead++;
				if ( _rtcBitsRead == 8 )
				{
					RtcBeginCommand();
				}
			}
		}
		else if ( (_rtcCommand & 0x80) == 0 )
		{
			OutputPins( 2 );

			if ( (_pinState & 1) == 0 )
			{
				_rtcBits &= ~(1 << _rtcBitsRead);
				_rtcBits |= ((_pinState >> 1) & 1) << _rtcBitsRead;
			}

			if ( !_rtcSckEdge && (_pinState & 1) != 0 )
			{
				if ( (((_rtcBits >> _rtcBitsRead) & 1) ^ ((_pinState >> 1) & 1)) != 0 )
				{
					_rtcBits &= ~(1 << _rtcBitsRead);
				}

				_rtcBitsRead++;
				if ( _rtcBitsRead == 8 )
				{
					RtcProcessByte();
				}
			}
		}
		else
		{
			if ( _rtcSckEdge && (_pinState & 1) == 0 )
			{
				_rtcSioOutput = RtcOutput();
				_rtcBitsRead++;
				if ( _rtcBitsRead == 8 )
				{
					_rtcBytesRemaining--;
					if ( _rtcBytesRemaining <= 0 )
					{
						_rtcBytesRemaining = RtcBytes[(_rtcCommand >> 4) & 7];
					}
					_rtcBitsRead = 0;
				}
			}
			OutputPins( _rtcSioOutput ? 2 : 0 );
		}

		_rtcSckEdge = (_pinState & 1) != 0;
	}

	private void RtcBeginCommand()
	{
		int command = _rtcBits;
		int magic = command & 0xF;

		if ( magic == 0x06 )
		{
			_rtcCommand = command;
			int cmd = (command >> 4) & 7;
			_rtcBytesRemaining = RtcBytes[cmd];
			_rtcCommandActive = true;

			switch ( cmd )
			{
				case 0:
					_rtcControl = 0;
					break;
				case 2:
				case 6:
					RtcUpdateClock();
					break;
				case 3:
				case 4:
					break;
			}
		}

		_rtcBits = 0;
		_rtcBitsRead = 0;
	}

	private void RtcProcessByte()
	{
		int cmd = (_rtcCommand >> 4) & 7;

		switch ( cmd )
		{
			case 4:
				_rtcControl = (byte)_rtcBits;
				break;
		}

		_rtcBits = 0;
		_rtcBitsRead = 0;
		_rtcBytesRemaining--;
		if ( _rtcBytesRemaining <= 0 )
		{
			_rtcBytesRemaining = RtcBytes[cmd];
		}
	}

	private bool RtcOutput()
	{
		byte outputByte = 0xFF;
		int cmd = (_rtcCommand >> 4) & 7;

		switch ( cmd )
		{
			case 4:
				outputByte = _rtcControl;
				break;
			case 2:
			case 6:
				outputByte = _rtcTime[7 - _rtcBytesRemaining];
				break;
		}

		return ((outputByte >> _rtcBitsRead) & 1) != 0;
	}

	private void RtcUpdateClock()
	{
		var now = DateTimeOffset.Now;
		_rtcTime[0] = RtcBcd( now.Year % 100 );
		_rtcTime[1] = RtcBcd( now.Month );
		_rtcTime[2] = RtcBcd( now.Day );
		_rtcTime[3] = RtcBcd( (int)now.DayOfWeek );

		bool hour24 = (_rtcControl & 0x40) != 0;
		if ( hour24 )
		{
			_rtcTime[4] = RtcBcd( now.Hour );
		}
		else
		{
			_rtcTime[4] = RtcBcd( now.Hour % 12 );
		}

		_rtcTime[5] = RtcBcd( now.Minute );
		_rtcTime[6] = RtcBcd( now.Second );
	}

	private static byte RtcBcd( int value )
	{
		return (byte)((value % 10) + ((value / 10 % 10) << 4));
	}

	public void InitRtc( byte[] rom )
	{
		if ( rom.Length < 0xB4 )
			return;

		string gameCode = System.Text.Encoding.ASCII.GetString( rom, 0xAC, 4 );
		if ( gameCode.StartsWith( "AXV", StringComparison.Ordinal ) || // Pokemon Ruby
			 gameCode.StartsWith( "AXP", StringComparison.Ordinal ) || // Pokemon Sapphire
			 gameCode.StartsWith( "BPE", StringComparison.Ordinal ) || // Pokemon Emerald
			 gameCode.StartsWith( "BLJ", StringComparison.Ordinal ) || // Legendz - Yomigaeru Shiren no Shima
			 gameCode.StartsWith( "BLV", StringComparison.Ordinal ) || // Legendz - Sign of Nekuromu
			 gameCode.StartsWith( "BR4", StringComparison.Ordinal ) || // RockMan EXE 4.5
			 gameCode.StartsWith( "BKA", StringComparison.Ordinal ) || // Sennen Kazoku
			 gameCode.StartsWith( "U3I", StringComparison.Ordinal ) || // Boktai
			 gameCode.StartsWith( "U32", StringComparison.Ordinal ) || // Boktai 2
			 gameCode.StartsWith( "U33", StringComparison.Ordinal ) )  // Boktai 3
		{
			HasRtc = true;
		}
	}

	public void Serialize( BinaryWriter w )
	{
		w.Write( _writeLatch );
		w.Write( _pinState );
		w.Write( _direction );
		w.Write( _readWrite );
		w.Write( _rtcBytesRemaining );
		w.Write( _rtcBitsRead );
		w.Write( _rtcBits );
		w.Write( _rtcCommandActive );
		w.Write( _rtcSckEdge );
		w.Write( _rtcSioOutput );
		w.Write( _rtcCommand );
		w.Write( _rtcControl );
		w.Write( _rtcTime );
	}

	public void Deserialize( BinaryReader r )
	{
		_writeLatch = r.ReadByte();
		_pinState = r.ReadByte();
		_direction = r.ReadByte();
		_readWrite = r.ReadBoolean();
		_rtcBytesRemaining = r.ReadInt32();
		_rtcBitsRead = r.ReadInt32();
		_rtcBits = r.ReadInt32();
		_rtcCommandActive = r.ReadBoolean();
		_rtcSckEdge = r.ReadBoolean();
		_rtcSioOutput = r.ReadBoolean();
		_rtcCommand = r.ReadInt32();
		_rtcControl = r.ReadByte();
		r.Read( _rtcTime );
	}
}