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(); // 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; } } }