diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
index 89b09fb05a..4a00050be4 100644
--- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
+++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
@@ -582,6 +582,8 @@
     </Compile>
     <Compile Include="Consoles\Nintendo\GBA\VBARegisterHelper.cs" />
     <Compile Include="Consoles\Nintendo\GBHawk\Audio.cs" />
+    <Compile Include="Consoles\Nintendo\GBHawk\Mappers\Mapper_Sachen_MMC2.cs" />
+    <Compile Include="Consoles\Nintendo\GBHawk\Mappers\Mapper_Sachen_MMC1.cs" />
     <Compile Include="Consoles\Nintendo\GBHawk\SerialPort.cs" />
     <Compile Include="Consoles\Nintendo\GBHawk\GBHawk.cs" />
     <Compile Include="Consoles\Nintendo\GBHawk\GBHawk.IDebuggable.cs" />
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.IMemoryDomains.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.IMemoryDomains.cs
index 162b39b6c2..375abbd592 100644
--- a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.IMemoryDomains.cs
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.IMemoryDomains.cs
@@ -51,7 +51,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 		private byte PeekSystemBus(long addr)
 		{
 			ushort addr2 = (ushort)(addr & 0xFFFF);
-			return ReadMemory(addr2);
+			return PeekMemory(addr2);
 		}
 
 		private void PokeSystemBus(long addr, byte value)
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.cs
index 5a4da37c45..61e366435c 100644
--- a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.cs
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.cs
@@ -68,7 +68,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 			{
 				ReadMemory = ReadMemory,
 				WriteMemory = WriteMemory,
-				PeekMemory = ReadMemory,
+				PeekMemory = PeekMemory,
 				DummyReadMemory = ReadMemory,
 				OnExecFetch = ExecFetch
 			};
@@ -138,7 +138,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 			audio.Reset();
 			serialport.Reset();
 
-			cpu.SetCallbacks(ReadMemory, ReadMemory, ReadMemory, WriteMemory);
+			cpu.SetCallbacks(ReadMemory, PeekMemory, PeekMemory, WriteMemory);
 
 			_vidbuffer = new int[VirtualWidth * VirtualHeight];
 		}
@@ -184,6 +184,11 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 				case 0xFE: mapper = new MapperHuC3();		mppr = "HuC3";		break;
 				case 0xFF: mapper = new MapperHuC1();		mppr = "HuC1";		break;
 
+				// Bootleg mappers
+				// NOTE: Sachen mapper selection does not account for scrambling, so if another bootleg mapper
+				// identifies itself as 0x31, this will need to be modified
+				case 0x31: mapper = new MapperSachen2();	mppr = "Schn2";		break;
+
 				case 0x4:
 				case 0x7:
 				case 0xA:
@@ -197,6 +202,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 				case 0x21:
 				default:
 					// mapper not implemented
+					Console.WriteLine(header[0x47]);
 					throw new Exception("Mapper not implemented");
 					break;
 
@@ -236,6 +242,12 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 					break;
 			}
 
+			// Sachen maper not known to have RAM
+			if ((mppr == "Schn1") || (mppr == "Schn2"))
+			{
+				cart_RAM = null;
+			}
+
 			// mbc2 carts have built in RAM
 			if (mppr == "MBC2")
 			{
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/Mappers/Mapper_Sachen_MMC1.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/Mappers/Mapper_Sachen_MMC1.cs
new file mode 100644
index 0000000000..b178fe1037
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/Mappers/Mapper_Sachen_MMC1.cs
@@ -0,0 +1,157 @@
+using BizHawk.Common;
+using BizHawk.Common.NumberExtensions;
+using System;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
+{
+	// Sachen Bootleg Mapper
+	// NOTE: Normally, locked mode is disabled after 31 rises of A15
+	// this occurs when the Boot Rom is loading the nintendo logo into VRAM
+	// instead of tracking that in the main memory map where it will just slow things down for no reason
+	// we'll clear the 'locked' flag when the last byte of the logo is read
+	class MapperSachen1 : MapperBase
+	{
+		public int ROM_bank;
+		public bool locked;
+		public int ROM_mask;
+		public int ROM_bank_mask;
+		public int BASE_ROM_Bank;
+		public bool reg_access;
+
+		public override void Initialize()
+		{
+			ROM_bank = 1;
+			ROM_mask = Core._rom.Length / 0x4000 - 1;
+			BASE_ROM_Bank = 0;
+			ROM_bank_mask = 0xFF;
+			locked = true;
+			reg_access = false;
+		}
+
+		public override byte ReadMemory(ushort addr)
+		{
+			if (addr < 0x4000)
+			{
+				ushort t_addr = addr;
+				
+				// header is scrambled
+				if ((addr >= 0x100) && (addr < 0x200))
+				{
+					int temp0 = (addr & 1);
+					int temp1 = (addr & 2);
+					int temp4 = (addr & 0x10);
+					int temp6 = (addr & 0x40);
+
+					temp0 = temp0 << 6;
+					temp1 = temp1 << 3;
+					temp4 = temp4 >> 3;
+					temp6 = temp6 >> 6;
+
+					addr &= 0x1AC;
+					addr |= (ushort)(temp0 | temp1 | temp4 | temp6);				
+				}
+
+				if (locked) { addr |= 0x80; }
+
+				if (t_addr == 0x133)
+				{
+					locked = false;
+					Console.WriteLine("cleared");
+				}
+
+				return Core._rom[addr + BASE_ROM_Bank * 0x4000];
+			}
+			else if (addr < 0x8000)
+			{
+				return Core._rom[(addr - 0x4000) + ROM_bank * 0x4000];
+			}
+			else
+			{
+				return 0xFF;
+			}
+		}
+
+		public override byte PeekMemory(ushort addr)
+		{
+			if (addr < 0x4000)
+			{
+				ushort t_addr = addr;
+
+				// header is scrambled
+				if ((addr >= 0x100) && (addr < 0x200))
+				{
+					int temp0 = (addr & 1);
+					int temp1 = (addr & 2);
+					int temp4 = (addr & 0x10);
+					int temp6 = (addr & 0x40);
+
+					temp0 = temp0 << 6;
+					temp1 = temp1 << 3;
+					temp4 = temp4 >> 3;
+					temp6 = temp6 >> 6;
+
+					addr &= 0x1AC;
+					addr |= (ushort)(temp0 | temp1 | temp4 | temp6);
+				}
+
+				if (locked) { addr |= 0x80; }
+
+				return Core._rom[addr + BASE_ROM_Bank * 0x4000];
+			}
+			else if (addr < 0x8000)
+			{
+				return Core._rom[(addr - 0x4000) + ROM_bank * 0x4000];
+			}
+			else
+			{
+				return 0xFF;
+			}
+		}
+
+		public override void WriteMemory(ushort addr, byte value)
+		{
+			if (addr < 0x2000)
+			{
+				if (reg_access)
+				{
+					BASE_ROM_Bank = value;
+				}
+			}
+			else if (addr < 0x4000)
+			{
+				ROM_bank = (value > 0) ? value : 1;
+
+				if ((value & 0x30) == 0x30)
+				{
+					reg_access = true;
+				}
+				else
+				{
+					reg_access = false;
+				}
+			}
+			else if (addr < 0x6000)
+			{
+				if (reg_access)
+				{
+					ROM_bank_mask = value;
+				}
+			}
+		}
+
+		public override void PokeMemory(ushort addr, byte value)
+		{
+			WriteMemory(addr, value);
+		}
+
+		public override void SyncState(Serializer ser)
+		{
+			ser.Sync("ROM_Bank", ref ROM_bank);
+			ser.Sync("ROM_Mask", ref ROM_mask);
+			ser.Sync("locked", ref locked);
+			ser.Sync("ROM_bank_mask", ref ROM_bank_mask);
+			ser.Sync("BASE_ROM_Bank", ref BASE_ROM_Bank);
+			ser.Sync("reg_access", ref reg_access);
+		}
+	}
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/Mappers/Mapper_Sachen_MMC2.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/Mappers/Mapper_Sachen_MMC2.cs
new file mode 100644
index 0000000000..d43e912cf8
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/Mappers/Mapper_Sachen_MMC2.cs
@@ -0,0 +1,159 @@
+using BizHawk.Common;
+using BizHawk.Common.NumberExtensions;
+using System;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
+{
+	// Sachen Bootleg Mapper
+	// NOTE: Normally, locked mode is disabled after 31 rises of A15
+	// this occurs when the Boot Rom is loading the nintendo logo into VRAM
+	// instead of tracking that in the main memory map where it will just slow things down for no reason
+	// we'll clear the 'locked' flag when the last byte of the logo is read
+	class MapperSachen2 : MapperBase
+	{
+		public int ROM_bank;
+		public bool locked;
+		public int ROM_mask;
+		public int ROM_bank_mask;
+		public int BASE_ROM_Bank;
+		public bool reg_access;
+
+		public override void Initialize()
+		{
+			ROM_bank = 1;
+			ROM_mask = Core._rom.Length / 0x4000 - 1;
+			BASE_ROM_Bank = 0;
+			ROM_bank_mask = 0;
+			locked = false;
+			reg_access = false;
+		}
+
+		public override byte ReadMemory(ushort addr)
+		{
+			if (addr < 0x4000)
+			{
+				ushort t_addr = addr;
+
+				// header is scrambled
+				if ((addr >= 0x100) && (addr < 0x200))
+				{
+					int temp0 = (addr & 1);
+					int temp1 = (addr & 2);
+					int temp4 = (addr & 0x10);
+					int temp6 = (addr & 0x40);
+
+					temp0 = temp0 << 6;
+					temp1 = temp1 << 3;
+					temp4 = temp4 >> 3;
+					temp6 = temp6 >> 6;
+
+					addr &= 0x1AC;
+					addr |= (ushort)(temp0 | temp1 | temp4 | temp6);
+				}
+
+				if (locked) { addr |= 0x80; }
+
+				if (t_addr == 0x133)
+				{
+					if ((Core.GB_bios_register & 0x1) == 0) { locked ^= true; }
+				}
+
+				return Core._rom[addr + BASE_ROM_Bank * 0x4000];
+			}
+			else if (addr < 0x8000)
+			{
+				int temp_bank = (ROM_bank & ~ROM_bank_mask) | (ROM_bank_mask & BASE_ROM_Bank);
+				temp_bank &= ROM_mask;
+
+				return Core._rom[(addr - 0x4000) + temp_bank * 0x4000];
+			}
+			else
+			{
+				return 0xFF;
+			}
+		}
+
+		public override byte PeekMemory(ushort addr)
+		{
+			if (addr < 0x4000)
+			{
+				ushort t_addr = addr;
+
+				// header is scrambled
+				if ((addr >= 0x100) && (addr < 0x200))
+				{
+					int temp0 = (addr & 1);
+					int temp1 = (addr & 2);
+					int temp4 = (addr & 0x10);
+					int temp6 = (addr & 0x40);
+
+					temp0 = temp0 << 6;
+					temp1 = temp1 << 3;
+					temp4 = temp4 >> 3;
+					temp6 = temp6 >> 6;
+
+					addr &= 0x1AC;
+					addr |= (ushort)(temp0 | temp1 | temp4 | temp6);
+				}
+
+				if (locked) { addr |= 0x80; }
+
+				return Core._rom[addr + BASE_ROM_Bank * 0x4000];
+			}
+			else if (addr < 0x8000)
+			{
+				return Core._rom[(addr - 0x4000) + ROM_bank * 0x4000];
+			}
+			else
+			{
+				return 0xFF;
+			}
+		}
+
+		public override void WriteMemory(ushort addr, byte value)
+		{
+			if (addr < 0x2000)
+			{
+				if (reg_access)
+				{
+					BASE_ROM_Bank = value;
+				}
+			}
+			else if (addr < 0x4000)
+			{
+				ROM_bank = (value > 0) ? (value) : 1;
+
+				if ((value & 0x30) == 0x30)
+				{
+					reg_access = true;
+				}
+				else
+				{
+					reg_access = false;
+				}
+			}
+			else if (addr < 0x6000)
+			{
+				if (reg_access)
+				{
+					ROM_bank_mask = value;
+				}
+			}
+		}
+
+		public override void PokeMemory(ushort addr, byte value)
+		{
+			WriteMemory(addr, value);
+		}
+
+		public override void SyncState(Serializer ser)
+		{
+			ser.Sync("ROM_Bank", ref ROM_bank);
+			ser.Sync("ROM_Mask", ref ROM_mask);
+			ser.Sync("locked", ref locked);
+			ser.Sync("ROM_bank_mask", ref ROM_bank_mask);
+			ser.Sync("BASE_ROM_Bank", ref BASE_ROM_Bank);
+			ser.Sync("reg_access", ref reg_access);
+		}
+	}
+}
\ No newline at end of file
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/MemoryMap.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/MemoryMap.cs
index 86d436bdde..3564245b4c 100644
--- a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/MemoryMap.cs
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/MemoryMap.cs
@@ -206,5 +206,96 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
 				Write_Registers(addr, value);
 			}
 		}
+
+		public byte PeekMemory(ushort addr)
+		{
+			if (ppu.DMA_start)
+			{
+				if ((addr >= 0xFE00) && (addr < 0xFEA0) && ppu.DMA_OAM_access)
+				{
+					return OAM[addr - 0xFE00];
+				}
+				else if ((addr >= 0xFF80))
+				{
+					return ZP_RAM[addr - 0xFF80];
+				}
+				else if ((addr >= 0xE000) && (addr < 0xFE00))
+				{
+					return RAM[addr - 0xE000]; // some of gekkio's tests require this to be accessible during DMA
+				}
+				else if (addr < 0x4000)
+				{
+					return mapper.PeekMemory(addr); // some of gekkio's tests require this to be accessible during DMA
+				}
+				return 0xFF;
+			}
+
+			if (addr < 0x100)
+			{
+				// return Either BIOS ROM or Game ROM
+				if ((GB_bios_register & 0x1) == 0)
+				{
+					return _bios[addr]; // Return BIOS
+				}
+				else
+				{
+					return mapper.PeekMemory(addr);
+				}
+			}
+			else if (addr < 0x8000)
+			{
+				return mapper.PeekMemory(addr);
+			}
+			else if (addr < 0x9800)
+			{
+				if (ppu.VRAM_access_read) { return CHR_RAM[addr - 0x8000]; }
+				else { return 0xFF; }
+			}
+			else if (addr < 0x9C00)
+			{
+				if (ppu.VRAM_access_read) { return BG_map_1[addr - 0x9800]; }
+				else { return 0xFF; }
+			}
+			else if (addr < 0xA000)
+			{
+				if (ppu.VRAM_access_read) { return BG_map_2[addr - 0x9C00]; }
+				else { return 0xFF; }
+			}
+			else if (addr < 0xC000)
+			{
+				return mapper.PeekMemory(addr);
+			}
+			else if (addr < 0xE000)
+			{
+				return RAM[addr - 0xC000];
+			}
+			else if (addr < 0xFE00)
+			{
+				return RAM[addr - 0xE000];
+			}
+			else if (addr < 0xFEA0)
+			{
+				if (ppu.OAM_access_read) { return OAM[addr - 0xFE00]; }
+				else { return 0xFF; }
+			}
+			else if (addr < 0xFF00)
+			{
+				// unmapped memory, returns 0xFF
+				return 0xFF;
+			}
+			else if (addr < 0xFF80)
+			{
+				return Read_Registers(addr);
+			}
+			else if (addr < 0xFFFF)
+			{
+				return ZP_RAM[addr - 0xFF80];
+			}
+			else
+			{
+				return Read_Registers(addr);
+			}
+
+		}
 	}
 }