266 lines
8.8 KiB
C#
266 lines
8.8 KiB
C#
using Core.Interfaces;
|
|
using System;
|
|
using System.IO;
|
|
|
|
namespace Core.Memory
|
|
{
|
|
public class SmsMemoryBus : IMemory
|
|
{
|
|
// SMS has 8KB of internal Work RAM
|
|
private readonly byte[] _workRam = new byte[0x2000];
|
|
|
|
// The entire game cartridge loaded into one big array
|
|
private byte[] _cartridgeRom = Array.Empty<byte>();
|
|
|
|
// The Paging Registers (Which 16KB chunk is in which slot?)
|
|
// Default startup state: Bank 0, 1, and 2 in order.
|
|
private int _romBank0 = 0;
|
|
private int _romBank1 = 1;
|
|
private int _romBank2 = 2;
|
|
|
|
// --- NEW: Cartridge RAM ---
|
|
private byte _mapperControl = 0; // 0xFFFC
|
|
private byte[] _cartridgeRam = new byte[0x8000]; // 32KB Max Cart RAM
|
|
|
|
// A flag to handle cartridges that don't use paging (like early 32KB games)
|
|
private bool _isCartridgeLoaded = false;
|
|
public bool SramUsed { get; private set; } = false;
|
|
|
|
public void LoadCartridge(byte[] romData)
|
|
{
|
|
_cartridgeRom = romData;
|
|
_isCartridgeLoaded = true;
|
|
|
|
// Reset the mapper on boot
|
|
_romBank0 = 0;
|
|
_romBank1 = 1;
|
|
_romBank2 = 2;
|
|
|
|
// Clean the RAM
|
|
CleanRAMData();
|
|
}
|
|
|
|
public byte Read(ushort address)
|
|
{
|
|
if (address < 0x4000)
|
|
{
|
|
if (address < 0x0400)
|
|
{
|
|
return _cartridgeRom[address];
|
|
}
|
|
// Slot 0
|
|
return _cartridgeRom[(_romBank0 * 0x4000) + address];
|
|
}
|
|
if (address < 0x8000)
|
|
{
|
|
// Slot 1
|
|
return _cartridgeRom[(_romBank1 * 0x4000) + (address - 0x4000)];
|
|
}
|
|
if (address < 0xC000)
|
|
{
|
|
// Slot 2 (Or SRAM)
|
|
if (SramUsed && (_mapperControl & 0x08) != 0)
|
|
return _cartridgeRam[address - 0x8000];
|
|
else
|
|
return _cartridgeRom[(_romBank2 * 0x4000) + (address - 0x8000)];
|
|
}
|
|
|
|
// THE FIX 1: Bitwise AND perfectly forces 0xE000-0xFFFF to mirror down to 0xC000!
|
|
return _workRam[address & 0x1FFF];
|
|
}
|
|
|
|
public void Write(ushort address, byte value)
|
|
{
|
|
if (address < 0x8000) return; // Cannot write to physical ROM
|
|
|
|
if (address < 0xC000)
|
|
{
|
|
if (SramUsed && (_mapperControl & 0x08) != 0)
|
|
_cartridgeRam[address - 0x8000] = value;
|
|
return;
|
|
}
|
|
|
|
// THE FIX 1: Save to RAM using the mirror mask
|
|
_workRam[address & 0x1FFF] = value;
|
|
|
|
// THE FIX 2: Mapper Control Registers live at the very end of the mirrored RAM!
|
|
if (address >= 0xFFFC)
|
|
{
|
|
// Calculate the absolute maximum number of banks inside the currently loaded ROM
|
|
int totalBanks = _cartridgeRom.Length / 0x4000;
|
|
|
|
// Safety catch for empty slots
|
|
if (totalBanks == 0) totalBanks = 1;
|
|
|
|
if (address == 0xFFFC)
|
|
{
|
|
_mapperControl = value;
|
|
}
|
|
else if (address == 0xFFFD)
|
|
{
|
|
_romBank0 = value % totalBanks; // Force the bank to stay inside the array bounds!
|
|
}
|
|
else if (address == 0xFFFE)
|
|
{
|
|
_romBank1 = value % totalBanks;
|
|
}
|
|
else if (address == 0xFFFF)
|
|
{
|
|
_romBank2 = value % totalBanks;
|
|
}
|
|
}
|
|
}
|
|
|
|
//public byte Read(ushort address)
|
|
//{
|
|
// if (address < 0x4000) // ROM Slot 0 (0x0000 - 0x3FFF)
|
|
// {
|
|
// // SMS Hardware Quirk: The first 1KB (0x0000 - 0x03FF) is NEVER paged.
|
|
// if (address < 0x0400) return ReadFromCartridge(0, address);
|
|
|
|
// return ReadFromCartridge(_romBank0, address);
|
|
// }
|
|
// if (address < 0x8000) // ROM Slot 1 (0x4000 - 0x7FFF)
|
|
// {
|
|
// return ReadFromCartridge(_romBank1, address & 0x3FFF);
|
|
// }
|
|
// if (address < 0xC000) // ROM Slot 2 (0x8000 - 0xBFFF)
|
|
// {
|
|
// // --- THE MISSING LINK: Read from Save RAM if enabled! ---
|
|
// if ((_mapperControl & 0x08) != 0)
|
|
// {
|
|
// int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0;
|
|
// int ramOffset = (ramBank * 0x4000) + (address & 0x1FFF);
|
|
// return _cartridgeRam[ramOffset];
|
|
// }
|
|
|
|
// // Otherwise, read from the ROM as usual
|
|
// return ReadFromCartridge(_romBank2, address & 0x3FFF);
|
|
// }
|
|
|
|
// // System RAM (0xC000 - 0xFFFF)
|
|
// return _workRam[address & 0x1FFF];
|
|
//}
|
|
|
|
|
|
//public void Write(ushort address, byte value)
|
|
//{
|
|
// if (address < 0xC000)
|
|
// {
|
|
// // Bypass the lock if they are trying to save their game!
|
|
// if (address >= 0x8000 && (_mapperControl & 0x08) != 0)
|
|
// {
|
|
// SramUsed = true;
|
|
// int ramBank = (_mapperControl & 0x04) != 0 ? 1 : 0;
|
|
// int ramOffset = (ramBank * 0x4000) + (address & 0x1FFF);
|
|
// _cartridgeRam[ramOffset] = value;
|
|
// return;
|
|
// }
|
|
|
|
// // You cannot write to a ROM cartridge!
|
|
// return;
|
|
// }
|
|
|
|
// // Write to System RAM
|
|
// _workRam[address & 0x1FFF] = value;
|
|
|
|
// // --- THE SEGA MAPPER ---
|
|
// if (address == 0xFFFC) // <-- ADD THIS
|
|
// {
|
|
// _mapperControl = value;
|
|
// }
|
|
// else if (address == 0xFFFD)
|
|
// {
|
|
// _romBank0 = value;
|
|
// }
|
|
|
|
// // Write to System RAM
|
|
// _workRam[address & 0x1FFF] = value;
|
|
|
|
// // --- THE SEGA MAPPER ---
|
|
// // If the CPU wrote to the very top of memory, it is commanding a bank swap!
|
|
// if (address == 0xFFFD)
|
|
// {
|
|
// _romBank0 = value;
|
|
// }
|
|
// else if (address == 0xFFFE)
|
|
// {
|
|
// _romBank1 = value;
|
|
// }
|
|
// else if (address == 0xFFFF)
|
|
// {
|
|
// _romBank2 = value;
|
|
// }
|
|
//}
|
|
|
|
private byte ReadFromCartridge(int bankIndex, int offset)
|
|
{
|
|
if (!_isCartridgeLoaded) return 0xFF; // Floating bus if no game
|
|
|
|
int absoluteAddress = (bankIndex * 0x4000) + offset;
|
|
|
|
// Safety check in case a game asks for a bank outside its file size
|
|
if (absoluteAddress >= _cartridgeRom.Length)
|
|
{
|
|
// Wrap around (mirrors the ROM if it's smaller than the requested bank)
|
|
absoluteAddress %= _cartridgeRom.Length;
|
|
}
|
|
|
|
return _cartridgeRom[absoluteAddress];
|
|
}
|
|
public void LoadSaveData(string filePath)
|
|
{
|
|
if (System.IO.File.Exists(filePath))
|
|
{
|
|
byte[] saveData = System.IO.File.ReadAllBytes(filePath);
|
|
Array.Copy(saveData, _cartridgeRam, Math.Min(saveData.Length, _cartridgeRam.Length));
|
|
SramUsed = true; // We loaded a save, so it's active!
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < _cartridgeRam.Length; i++)
|
|
{
|
|
_cartridgeRam[i] = 0xFF;
|
|
}
|
|
SramUsed = false;
|
|
}
|
|
}
|
|
|
|
public void SaveSaveData(string filePath)
|
|
{
|
|
// Only write a file to the hard drive if the game actually used the Save RAM!
|
|
if (SramUsed)
|
|
{
|
|
System.IO.File.WriteAllBytes(filePath, _cartridgeRam);
|
|
}
|
|
}
|
|
|
|
public void SaveState(BinaryWriter bw)
|
|
{
|
|
bw.Write(_workRam);
|
|
bw.Write(_cartridgeRam);
|
|
bw.Write(SramUsed);
|
|
bw.Write(_mapperControl);
|
|
bw.Write(_romBank0);
|
|
bw.Write(_romBank1);
|
|
bw.Write(_romBank2);
|
|
}
|
|
|
|
public void LoadState(BinaryReader br)
|
|
{
|
|
Array.Copy(br.ReadBytes(_workRam.Length), _workRam, _workRam.Length);
|
|
Array.Copy(br.ReadBytes(_cartridgeRam.Length), _cartridgeRam, _cartridgeRam.Length);
|
|
SramUsed = br.ReadBoolean();
|
|
_mapperControl = br.ReadByte();
|
|
_romBank0 = br.ReadInt32();
|
|
_romBank1 = br.ReadInt32();
|
|
_romBank2 = br.ReadInt32();
|
|
}
|
|
|
|
public void CleanRAMData()
|
|
{
|
|
Array.Clear(_workRam, 0, _workRam.Length);
|
|
_mapperControl = 0;
|
|
}
|
|
}
|
|
} |