diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
index e5e57c71bc..f589bfa790 100644
--- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
+++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
@@ -839,6 +839,31 @@
VBANext.cs
+
+
+
+ GBHawkLink4x.cs
+
+
+ GBHawkLink4x.cs
+
+
+ GBHawkLink4x.cs
+
+
+ GBHawkLink4x.cs
+
+
+ GBHawkLink4x.cs
+
+
+ GBHawkLink4x.cs
+
+
+ GBHawkLink4x.cs
+
+
+
@@ -940,7 +965,7 @@
PPU.cs
-
+
PPU.cs
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISaveRam.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISaveRam.cs
index 45dff8cd95..0fd92601bb 100644
--- a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISaveRam.cs
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISaveRam.cs
@@ -36,7 +36,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink
if (R.cart_RAM != null)
{
- for (int i = 0; i < L.cart_RAM.Length; i++)
+ for (int i = 0; i < R.cart_RAM.Length; i++)
{
temp[index] = R.cart_RAM[i];
index++;
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISaveRam.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISaveRam.cs
index d4a7264484..ee6ea5ef72 100644
--- a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISaveRam.cs
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISaveRam.cs
@@ -51,7 +51,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink3x
if (R.cart_RAM != null)
{
- for (int i = 0; i < L.cart_RAM.Length; i++)
+ for (int i = 0; i < R.cart_RAM.Length; i++)
{
temp[index] = R.cart_RAM[i];
index++;
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ICodeDataLog.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ICodeDataLog.cs
new file mode 100644
index 0000000000..02933085e8
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ICodeDataLog.cs
@@ -0,0 +1,185 @@
+using System;
+using System.IO;
+
+using BizHawk.Emulation.Common;
+using BizHawk.Emulation.Common.Components.LR35902;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : ICodeDataLogger
+ {
+ private ICodeDataLog _cdl;
+
+ public void SetCDL(ICodeDataLog cdl)
+ {
+ _cdl = cdl;
+ if (cdl == null)
+ this.A.cpu.CDLCallback = null;
+ else this.A.cpu.CDLCallback = CDLCpuCallback;
+ }
+
+ public void NewCDL(ICodeDataLog cdl)
+ {
+ cdl["ROM"] = new byte[MemoryDomains["ROM A"].Size];
+ cdl["HRAM"] = new byte[MemoryDomains["Zero Page RAM A"].Size];
+
+ cdl["WRAM"] = new byte[MemoryDomains["Main RAM A"].Size];
+
+ if (MemoryDomains.Has("Cart RAM A"))
+ {
+ cdl["CartRAM"] = new byte[MemoryDomains["Cart RAM A"].Size];
+ }
+
+ cdl.SubType = "GB";
+ cdl.SubVer = 0;
+ }
+
+ [FeatureNotImplemented]
+ void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
+ {
+ }
+
+ public void SetCDL(LR35902.eCDLogMemFlags flags, string type, int cdladdr)
+ {
+ if (type == null) return;
+ byte val = (byte)flags;
+ _cdl[type][cdladdr] |= (byte)flags;
+ }
+
+ void CDLCpuCallback(ushort addr, LR35902.eCDLogMemFlags flags)
+ {
+ if (addr < 0x8000)
+ {
+ //don't record writes to the ROM, it's just noisy
+ //NOTE: in principle a mapper could mount a useful resource here, but I doubt it)
+ if ((flags & LR35902.eCDLogMemFlags.Write) != 0) return;
+ }
+
+ if (A.ppu.DMA_start)
+ {
+ // some of gekkio's tests require these to be accessible during DMA
+ if (addr < 0x8000)
+ {
+ if (A.ppu.DMA_addr < 0x80)
+ {
+ return;
+ }
+ else
+ {
+ A.mapper.MapCDL(addr, flags);
+ return;
+ }
+ }
+ else if ((addr >= 0xE000) && (addr < 0xF000))
+ {
+ SetCDL(flags, "WRAM", addr - 0xE000);
+ }
+ else if ((addr >= 0xF000) && (addr < 0xFE00))
+ {
+ SetCDL(flags, "WRAM", (A.RAM_Bank * 0x1000) + (addr - 0xF000));
+ }
+ else if ((addr >= 0xFE00) && (addr < 0xFEA0) && A.ppu.DMA_OAM_access)
+ {
+ return;
+ }
+ else if ((addr >= 0xFF00) && (addr < 0xFF80)) // The game GOAL! Requires Hardware Regs to be accessible
+ {
+ return;
+ }
+ else if ((addr >= 0xFF80))
+ {
+ SetCDL(flags, "HRAM", addr - 0xFF80);
+ }
+
+ }
+
+ if (addr < 0x900)
+ {
+ if (addr < 0x100)
+ {
+ // return Either BIOS ROM or Game ROM
+ if ((A.GB_bios_register & 0x1) == 0)
+ {
+ return;
+ }
+ else
+ {
+ A.mapper.MapCDL(addr, flags);
+ return;
+ }
+ }
+ else if (addr >= 0x200)
+ {
+ // return Either BIOS ROM or Game ROM
+ if (((A.GB_bios_register & 0x1) == 0) && A.is_GBC)
+ {
+ return;
+ }
+ else
+ {
+ A.mapper.MapCDL(addr, flags);
+ return;
+ }
+ }
+ else
+ {
+ A.mapper.MapCDL(addr, flags);
+ return;
+ }
+ }
+ else if (addr < 0x8000)
+ {
+ A.mapper.MapCDL(addr, flags);
+ return;
+ }
+ else if (addr < 0xA000)
+ {
+ return;
+ }
+ else if (addr < 0xC000)
+ {
+ A.mapper.MapCDL(addr, flags);
+ return;
+ }
+ else if (addr < 0xD000)
+ {
+ return;
+ }
+ else if (addr < 0xE000)
+ {
+ SetCDL(flags, "WRAM", (A.RAM_Bank * 0x1000) + (addr - 0xD000));
+ }
+ else if (addr < 0xF000)
+ {
+ SetCDL(flags, "WRAM", addr - 0xE000);
+ }
+ else if (addr < 0xFE00)
+ {
+ SetCDL(flags, "WRAM", (A.RAM_Bank * 0x1000) + (addr - 0xF000));
+ }
+ else if (addr < 0xFEA0)
+ {
+ return;
+ }
+ else if (addr < 0xFF00)
+ {
+ return;
+ }
+ else if (addr < 0xFF80)
+ {
+ return;
+ }
+ else if (addr < 0xFFFF)
+ {
+ SetCDL(flags, "HRAM", addr - 0xFF80);
+ }
+ else
+ {
+ return;
+ }
+
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IDebuggable.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IDebuggable.cs
new file mode 100644
index 0000000000..cf63fea6e8
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IDebuggable.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : IDebuggable
+ {
+ public IDictionary GetCpuFlagsAndRegisters()
+ {
+ return new Dictionary
+ {
+ /*
+ ["A"] = cpu.A,
+ ["X"] = cpu.X,
+ ["Y"] = cpu.Y,
+ ["S"] = cpu.S,
+ ["PC"] = cpu.PC,
+ ["Flag C"] = cpu.FlagC,
+ ["Flag Z"] = cpu.FlagZ,
+ ["Flag I"] = cpu.FlagI,
+ ["Flag D"] = cpu.FlagD,
+ ["Flag B"] = cpu.FlagB,
+ ["Flag V"] = cpu.FlagV,
+ ["Flag N"] = cpu.FlagN,
+ ["Flag T"] = cpu.FlagT
+ */
+ };
+ }
+
+ public void SetCpuRegister(string register, int value)
+ {
+ switch (register)
+ {
+ default:
+ throw new InvalidOperationException();
+ case "A":
+ //cpu.A = (byte)value;
+ break;
+ case "X":
+ //cpu.X = (byte)value;
+ break;
+ case "Y":
+ //cpu.Y = (byte)value;
+ break;
+ case "S":
+ //cpu.S = (byte)value;
+ break;
+ case "PC":
+ //cpu.PC = (ushort)value;
+ break;
+ case "Flag I":
+ //cpu.FlagI = value > 0;
+ break;
+ }
+ }
+
+ public IMemoryCallbackSystem MemoryCallbacks { get; } = new MemoryCallbackSystem(new[] { "System Bus" });
+
+ public bool CanStep(StepType type)
+ {
+ return false;
+ }
+
+ [FeatureNotImplemented]
+ public void Step(StepType type)
+ {
+ throw new NotImplementedException();
+ }
+
+ public long TotalExecutedCycles
+ {
+ get { return (long)A.cpu.TotalExecutedCycles; }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IEmulator.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IEmulator.cs
new file mode 100644
index 0000000000..466beae69e
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IEmulator.cs
@@ -0,0 +1,493 @@
+using System;
+
+using BizHawk.Emulation.Common;
+using BizHawk.Emulation.Cores.Nintendo.GBHawk;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : IEmulator, IVideoProvider, ISoundProvider
+ {
+ public IEmulatorServiceProvider ServiceProvider { get; }
+
+ public ControllerDefinition ControllerDefinition => _controllerDeck.Definition;
+
+ public bool FrameAdvance(IController controller, bool render, bool rendersound)
+ {
+ //Console.WriteLine("-----------------------FRAME-----------------------");
+ //Update the color palette if a setting changed
+ if (Link4xSettings.Palette_A == GBHawk.GBHawk.GBSettings.PaletteType.BW)
+ {
+ A.color_palette[0] = color_palette_BW[0];
+ A.color_palette[1] = color_palette_BW[1];
+ A.color_palette[2] = color_palette_BW[2];
+ A.color_palette[3] = color_palette_BW[3];
+ }
+ else
+ {
+ A.color_palette[0] = color_palette_Gr[0];
+ A.color_palette[1] = color_palette_Gr[1];
+ A.color_palette[2] = color_palette_Gr[2];
+ A.color_palette[3] = color_palette_Gr[3];
+ }
+
+ if (Link4xSettings.Palette_B == GBHawk.GBHawk.GBSettings.PaletteType.BW)
+ {
+ B.color_palette[0] = color_palette_BW[0];
+ B.color_palette[1] = color_palette_BW[1];
+ B.color_palette[2] = color_palette_BW[2];
+ B.color_palette[3] = color_palette_BW[3];
+ }
+ else
+ {
+ B.color_palette[0] = color_palette_Gr[0];
+ B.color_palette[1] = color_palette_Gr[1];
+ B.color_palette[2] = color_palette_Gr[2];
+ B.color_palette[3] = color_palette_Gr[3];
+ }
+
+ if (Link4xSettings.Palette_C == GBHawk.GBHawk.GBSettings.PaletteType.BW)
+ {
+ C.color_palette[0] = color_palette_BW[0];
+ C.color_palette[1] = color_palette_BW[1];
+ C.color_palette[2] = color_palette_BW[2];
+ C.color_palette[3] = color_palette_BW[3];
+ }
+ else
+ {
+ C.color_palette[0] = color_palette_Gr[0];
+ C.color_palette[1] = color_palette_Gr[1];
+ C.color_palette[2] = color_palette_Gr[2];
+ C.color_palette[3] = color_palette_Gr[3];
+ }
+
+ if (Link4xSettings.Palette_D == GBHawk.GBHawk.GBSettings.PaletteType.BW)
+ {
+ D.color_palette[0] = color_palette_BW[0];
+ D.color_palette[1] = color_palette_BW[1];
+ D.color_palette[2] = color_palette_BW[2];
+ D.color_palette[3] = color_palette_BW[3];
+ }
+ else
+ {
+ D.color_palette[0] = color_palette_Gr[0];
+ D.color_palette[1] = color_palette_Gr[1];
+ D.color_palette[2] = color_palette_Gr[2];
+ D.color_palette[3] = color_palette_Gr[3];
+ }
+
+ if (_tracer.Enabled)
+ {
+ A.cpu.TraceCallback = s => _tracer.Put(s);
+ }
+ else
+ {
+ A.cpu.TraceCallback = null;
+ }
+
+ _frame++;
+
+ if (controller.IsPressed("Power"))
+ {
+ HardReset();
+ }
+
+ if (controller.IsPressed("Toggle Cable LC") | controller.IsPressed("Toggle Cable CR") | controller.IsPressed("Toggle Cable RL"))
+ {
+ // if any connection exists, disconnect it
+ // otherwise connect in order of precedence
+ // only one event can happen per frame, either a connection or disconnection
+ if (_cableconnected_LC | _cableconnected_CR | _cableconnected_RL)
+ {
+ _cableconnected_LC = _cableconnected_CR = _cableconnected_RL = false;
+ do_2_next = false;
+ }
+ else if (controller.IsPressed("Toggle Cable LC"))
+ {
+ _cableconnected_LC = true;
+ }
+ else if (controller.IsPressed("Toggle Cable CR"))
+ {
+ _cableconnected_CR = true;
+ }
+ else if (controller.IsPressed("Toggle Cable RL"))
+ {
+ _cableconnected_RL = true;
+ }
+
+ Console.WriteLine("Cable connect status:");
+ Console.WriteLine("LC: " + _cableconnected_LC);
+ Console.WriteLine("CR: " + _cableconnected_CR);
+ Console.WriteLine("RL: " + _cableconnected_RL);
+ }
+
+ _islag = true;
+
+ GetControllerState(controller);
+
+ do_frame_fill = false;
+ do_frame();
+ if (do_frame_fill)
+ {
+ FillVideoBuffer();
+ }
+
+ _islag = A._islag & B._islag & C._islag & D._islag;
+
+ if (_islag)
+ {
+ _lagcount++;
+ }
+
+ return true;
+ }
+
+ public void do_frame()
+ {
+ // advance one full frame
+ for (int i = 0; i < 70224; i++)
+ {
+ A.do_single_step();
+ B.do_single_step();
+ C.do_single_step();
+ D.do_single_step();
+
+ if (_cableconnected_LC)
+ {
+ // the signal to shift out a bit is when serial_clock = 1
+ if (((A.serialport.serial_clock == 1) || (A.serialport.serial_clock == 2)) && (A.serialport.clk_rate > 0) && !do_2_next)
+ {
+ A.serialport.going_out = (byte)(A.serialport.serial_data >> 7);
+
+ if ((C.serialport.clk_rate == -1) && C.serialport.serial_start && A.serialport.can_pulse)
+ {
+ C.serialport.serial_clock = A.serialport.serial_clock;
+ C.serialport.going_out = (byte)(C.serialport.serial_data >> 7);
+ C.serialport.coming_in = A.serialport.going_out;
+ }
+
+ A.serialport.coming_in = C.serialport.going_out;
+ A.serialport.can_pulse = false;
+ }
+ else if (((C.serialport.serial_clock == 1) || (C.serialport.serial_clock == 2)) && (C.serialport.clk_rate > 0))
+ {
+ do_2_next = false;
+
+ C.serialport.going_out = (byte)(C.serialport.serial_data >> 7);
+
+ if ((A.serialport.clk_rate == -1) && A.serialport.serial_start && C.serialport.can_pulse)
+ {
+ A.serialport.serial_clock = C.serialport.serial_clock;
+ A.serialport.going_out = (byte)(A.serialport.serial_data >> 7);
+ A.serialport.coming_in = C.serialport.going_out;
+ }
+
+ C.serialport.coming_in = A.serialport.going_out;
+ C.serialport.can_pulse = false;
+
+ if (C.serialport.serial_clock == 2) { do_2_next = true; }
+ }
+ else
+ {
+ do_2_next = false;
+ }
+ }
+ else if (_cableconnected_CR)
+ {
+ // the signal to shift out a bit is when serial_clock = 1
+ if (((C.serialport.serial_clock == 1) || (C.serialport.serial_clock == 2)) && (C.serialport.clk_rate > 0) && !do_2_next)
+ {
+ C.serialport.going_out = (byte)(C.serialport.serial_data >> 7);
+
+ if ((D.serialport.clk_rate == -1) && D.serialport.serial_start && C.serialport.can_pulse)
+ {
+ D.serialport.serial_clock = C.serialport.serial_clock;
+ D.serialport.going_out = (byte)(D.serialport.serial_data >> 7);
+ D.serialport.coming_in = C.serialport.going_out;
+ }
+
+ C.serialport.coming_in = D.serialport.going_out;
+ C.serialport.can_pulse = false;
+ }
+ else if (((D.serialport.serial_clock == 1) || (D.serialport.serial_clock == 2)) && (D.serialport.clk_rate > 0))
+ {
+ do_2_next = false;
+
+ D.serialport.going_out = (byte)(D.serialport.serial_data >> 7);
+
+ if ((C.serialport.clk_rate == -1) && C.serialport.serial_start && D.serialport.can_pulse)
+ {
+ C.serialport.serial_clock = D.serialport.serial_clock;
+ C.serialport.going_out = (byte)(C.serialport.serial_data >> 7);
+ C.serialport.coming_in = D.serialport.going_out;
+ }
+
+ D.serialport.coming_in = C.serialport.going_out;
+ D.serialport.can_pulse = false;
+
+ if (D.serialport.serial_clock == 2) { do_2_next = true; }
+ }
+ else
+ {
+ do_2_next = false;
+ }
+ }
+ else if (_cableconnected_RL)
+ {
+ // the signal to shift out a bit is when serial_clock = 1
+ if (((D.serialport.serial_clock == 1) || (D.serialport.serial_clock == 2)) && (D.serialport.clk_rate > 0) && !do_2_next)
+ {
+ D.serialport.going_out = (byte)(D.serialport.serial_data >> 7);
+
+ if ((A.serialport.clk_rate == -1) && A.serialport.serial_start && D.serialport.can_pulse)
+ {
+ A.serialport.serial_clock = D.serialport.serial_clock;
+ A.serialport.going_out = (byte)(A.serialport.serial_data >> 7);
+ A.serialport.coming_in = D.serialport.going_out;
+ }
+
+ D.serialport.coming_in = A.serialport.going_out;
+ D.serialport.can_pulse = false;
+ }
+ else if (((A.serialport.serial_clock == 1) || (A.serialport.serial_clock == 2)) && (A.serialport.clk_rate > 0))
+ {
+ do_2_next = false;
+
+ A.serialport.going_out = (byte)(A.serialport.serial_data >> 7);
+
+ if ((D.serialport.clk_rate == -1) && D.serialport.serial_start && A.serialport.can_pulse)
+ {
+ D.serialport.serial_clock = A.serialport.serial_clock;
+ D.serialport.going_out = (byte)(D.serialport.serial_data >> 7);
+ D.serialport.coming_in = A.serialport.going_out;
+ }
+
+ A.serialport.coming_in = D.serialport.going_out;
+ A.serialport.can_pulse = false;
+
+ if (A.serialport.serial_clock == 2) { do_2_next = true; }
+ }
+ else
+ {
+ do_2_next = false;
+ }
+ }
+
+
+ // if we hit a frame boundary, update video
+ if (A.vblank_rise)
+ {
+ // update the controller state on VBlank
+ A.controller_state = A_controller;
+
+ // check if controller state caused interrupt
+ A.do_controller_check();
+
+ // send the image on VBlank
+ A.SendVideoBuffer();
+
+ A.vblank_rise = false;
+ do_frame_fill = true;
+ }
+ if (B.vblank_rise)
+ {
+ // update the controller state on VBlank
+ B.controller_state = B_controller;
+
+ // check if controller state caused interrupt
+ B.do_controller_check();
+
+ // send the image on VBlank
+ B.SendVideoBuffer();
+
+ B.vblank_rise = false;
+ do_frame_fill = true;
+ }
+ if (C.vblank_rise)
+ {
+ // update the controller state on VBlank
+ C.controller_state = C_controller;
+
+ // check if controller state caused interrupt
+ C.do_controller_check();
+
+ // send the image on VBlank
+ C.SendVideoBuffer();
+
+ C.vblank_rise = false;
+ do_frame_fill = true;
+ }
+ if (D.vblank_rise)
+ {
+ // update the controller state on VBlank
+ D.controller_state = D_controller;
+
+ // check if controller state caused interrupt
+ D.do_controller_check();
+
+ // send the image on VBlank
+ D.SendVideoBuffer();
+
+ D.vblank_rise = false;
+ do_frame_fill = true;
+ }
+ }
+ }
+
+ public void GetControllerState(IController controller)
+ {
+ InputCallbacks.Call();
+ A_controller = _controllerDeck.ReadPort1(controller);
+ B_controller = _controllerDeck.ReadPort2(controller);
+ C_controller = _controllerDeck.ReadPort3(controller);
+ D_controller = _controllerDeck.ReadPort4(controller);
+ }
+
+ public int Frame => _frame;
+
+ public string SystemId => "GB4x";
+
+ public bool DeterministicEmulation { get; set; }
+
+ public void ResetCounters()
+ {
+ _frame = 0;
+ _lagcount = 0;
+ _islag = false;
+ }
+
+ public CoreComm CoreComm { get; }
+
+ public void Dispose()
+ {
+ A.Dispose();
+ B.Dispose();
+ C.Dispose();
+ D.Dispose();
+ }
+
+ #region Video provider
+
+ public int _frameHz = 60;
+
+ public int[] _vidbuffer = new int[160 * 2 * 144 * 2];
+
+ public int[] GetVideoBuffer()
+ {
+ return _vidbuffer;
+ }
+
+ public void FillVideoBuffer()
+ {
+ // combine the 2 video buffers from the instances
+ for (int i = 0; i < 144; i++)
+ {
+ for (int j = 0; j < 160; j++)
+ {
+ _vidbuffer[i * 320 + j] = A.frame_buffer[i * 160 + j];
+ _vidbuffer[(i + 144) * 320 + j] = C.frame_buffer[i * 160 + j];
+ _vidbuffer[(i + 144) * 320 + j + 160] = C.frame_buffer[i * 160 + j];
+ _vidbuffer[i * 320 + j + 160] = D.frame_buffer[i * 160 + j];
+ }
+ }
+ }
+
+ public int VirtualWidth => 160 * 2;
+ public int VirtualHeight => 144 * 2;
+ public int BufferWidth => 160 * 2;
+ public int BufferHeight => 144 * 2;
+ public int BackgroundColor => unchecked((int)0xFF000000);
+ public int VsyncNumerator => _frameHz;
+ public int VsyncDenominator => 1;
+
+ public static readonly uint[] color_palette_BW = { 0xFFFFFFFF , 0xFFAAAAAA, 0xFF555555, 0xFF000000 };
+ public static readonly uint[] color_palette_Gr = { 0xFFA4C505, 0xFF88A905, 0xFF1D551D, 0xFF052505 };
+
+ public uint[] color_palette = new uint[4];
+
+ #endregion
+
+ #region audio
+
+ public bool CanProvideAsync => false;
+
+ public void SetSyncMode(SyncSoundMode mode)
+ {
+ if (mode != SyncSoundMode.Sync)
+ {
+ throw new InvalidOperationException("Only Sync mode is supported_");
+ }
+ }
+
+ public SyncSoundMode SyncMode => SyncSoundMode.Sync;
+
+ public void GetSamplesSync(out short[] samples, out int nsamp)
+ {
+ short[] temp_samp_A;
+ short[] temp_samp_B;
+ short[] temp_samp_C;
+ short[] temp_samp_D;
+
+ int nsamp_A;
+ int nsamp_B;
+ int nsamp_C;
+ int nsamp_D;
+
+ A.audio.GetSamplesSync(out temp_samp_A, out nsamp_A);
+ B.audio.GetSamplesSync(out temp_samp_B, out nsamp_B);
+ C.audio.GetSamplesSync(out temp_samp_C, out nsamp_C);
+ D.audio.GetSamplesSync(out temp_samp_D, out nsamp_D);
+
+ if (Link4xSettings.AudioSet == GBLink4xSettings.AudioSrc.A)
+ {
+ samples = temp_samp_A;
+ nsamp = nsamp_A;
+ }
+ else if (Link4xSettings.AudioSet == GBLink4xSettings.AudioSrc.B)
+ {
+ samples = temp_samp_C;
+ nsamp = nsamp_C;
+ }
+ else if (Link4xSettings.AudioSet == GBLink4xSettings.AudioSrc.C)
+ {
+ samples = temp_samp_C;
+ nsamp = nsamp_C;
+ }
+ else if (Link4xSettings.AudioSet == GBLink4xSettings.AudioSrc.D)
+ {
+ samples = temp_samp_D;
+ nsamp = nsamp_D;
+ }
+ else
+ {
+ samples = new short[0];
+ nsamp = 0;
+ }
+ }
+
+ public void GetSamplesAsync(short[] samples)
+ {
+ throw new NotSupportedException("Async is not available");
+ }
+
+ public void DiscardSamples()
+ {
+ A.audio.DiscardSamples();
+ C.audio.DiscardSamples();
+ D.audio.DiscardSamples();
+ }
+
+ private void GetSamples(short[] samples)
+ {
+
+ }
+
+ public void DisposeSound()
+ {
+ A.audio.DisposeSound();
+ C.audio.DisposeSound();
+ D.audio.DisposeSound();
+ }
+
+ #endregion
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IInputPollable.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IInputPollable.cs
new file mode 100644
index 0000000000..26d110808f
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IInputPollable.cs
@@ -0,0 +1,24 @@
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : IInputPollable
+ {
+ public int LagCount
+ {
+ get { return _lagcount; }
+ set { _lagcount = value; }
+ }
+
+ public bool IsLagFrame
+ {
+ get { return _islag; }
+ set { _islag = value; }
+ }
+
+ public IInputCallbackSystem InputCallbacks { get; } = new InputCallbackSystem();
+
+ public bool _islag = true;
+ private int _lagcount;
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IMemoryDomains.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IMemoryDomains.cs
new file mode 100644
index 0000000000..8ef115c4bc
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IMemoryDomains.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x
+ {
+ private IMemoryDomains MemoryDomains;
+
+ public void SetupMemoryDomains()
+ {
+ var domains = new List
+ {
+ new MemoryDomainDelegate(
+ "Main RAM A",
+ A.RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => A.RAM[addr],
+ (addr, value) => A.RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Main RAM B",
+ B.RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => B.RAM[addr],
+ (addr, value) => B.RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Main RAM C",
+ C.RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => C.RAM[addr],
+ (addr, value) => C.RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Main RAM D",
+ D.RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => D.RAM[addr],
+ (addr, value) => D.RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Zero Page RAM A",
+ A.ZP_RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => A.ZP_RAM[addr],
+ (addr, value) => A.ZP_RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Zero Page RAM B",
+ B.ZP_RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => B.ZP_RAM[addr],
+ (addr, value) => B.ZP_RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Zero Page RAM C",
+ C.ZP_RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => C.ZP_RAM[addr],
+ (addr, value) => C.ZP_RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "Zero Page RAM D",
+ D.ZP_RAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => D.ZP_RAM[addr],
+ (addr, value) => D.ZP_RAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "System Bus A",
+ 0X10000,
+ MemoryDomain.Endian.Little,
+ addr => PeekSystemBusA(addr),
+ (addr, value) => PokeSystemBusA(addr, value),
+ 1),
+ new MemoryDomainDelegate(
+ "System Bus B",
+ 0X10000,
+ MemoryDomain.Endian.Little,
+ addr => PeekSystemBusB(addr),
+ (addr, value) => PokeSystemBusB(addr, value),
+ 1),
+ new MemoryDomainDelegate(
+ "System Bus C",
+ 0X10000,
+ MemoryDomain.Endian.Little,
+ addr => PeekSystemBusC(addr),
+ (addr, value) => PokeSystemBusC(addr, value),
+ 1),
+ new MemoryDomainDelegate(
+ "System Bus D",
+ 0X10000,
+ MemoryDomain.Endian.Little,
+ addr => PeekSystemBusD(addr),
+ (addr, value) => PokeSystemBusD(addr, value),
+ 1),
+ new MemoryDomainDelegate(
+ "ROM A",
+ A._rom.Length,
+ MemoryDomain.Endian.Little,
+ addr => A._rom[addr],
+ (addr, value) => A._rom[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "ROM B",
+ B._rom.Length,
+ MemoryDomain.Endian.Little,
+ addr => B._rom[addr],
+ (addr, value) => B._rom[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "ROM C",
+ C._rom.Length,
+ MemoryDomain.Endian.Little,
+ addr => C._rom[addr],
+ (addr, value) => C._rom[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "ROM D",
+ D._rom.Length,
+ MemoryDomain.Endian.Little,
+ addr => D._rom[addr],
+ (addr, value) => D._rom[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "VRAM A",
+ A.VRAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => A.VRAM[addr],
+ (addr, value) => A.VRAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "VRAM B",
+ B.VRAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => B.VRAM[addr],
+ (addr, value) => B.VRAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "VRAM C",
+ C.VRAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => C.VRAM[addr],
+ (addr, value) => C.VRAM[addr] = value,
+ 1),
+ new MemoryDomainDelegate(
+ "VRAM D",
+ D.VRAM.Length,
+ MemoryDomain.Endian.Little,
+ addr => D.VRAM[addr],
+ (addr, value) => D.VRAM[addr] = value,
+ 1)
+ };
+
+ if (A.cart_RAM != null)
+ {
+ var CartRamA = new MemoryDomainByteArray("Cart RAM L", MemoryDomain.Endian.Little, A.cart_RAM, true, 1);
+ domains.Add(CartRamA);
+ }
+
+ if (B.cart_RAM != null)
+ {
+ var CartRamB = new MemoryDomainByteArray("Cart RAM B", MemoryDomain.Endian.Little, B.cart_RAM, true, 1);
+ domains.Add(CartRamB);
+ }
+
+ if (C.cart_RAM != null)
+ {
+ var CartRamC = new MemoryDomainByteArray("Cart RAM C", MemoryDomain.Endian.Little, C.cart_RAM, true, 1);
+ domains.Add(CartRamC);
+ }
+
+ if (D.cart_RAM != null)
+ {
+ var CartRamD = new MemoryDomainByteArray("Cart RAM D", MemoryDomain.Endian.Little, D.cart_RAM, true, 1);
+ domains.Add(CartRamD);
+ }
+
+ MemoryDomains = new MemoryDomainList(domains);
+ (ServiceProvider as BasicServiceProvider).Register(MemoryDomains);
+ }
+
+ private byte PeekSystemBusA(long addr)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ return A.PeekMemory(addr2);
+ }
+
+ private byte PeekSystemBusB(long addr)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ return B.PeekMemory(addr2);
+ }
+
+ private byte PeekSystemBusC(long addr)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ return C.PeekMemory(addr2);
+ }
+
+ private byte PeekSystemBusD(long addr)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ return D.PeekMemory(addr2);
+ }
+
+ private void PokeSystemBusA(long addr, byte value)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ A.WriteMemory(addr2, value);
+ }
+
+ private void PokeSystemBusB(long addr, byte value)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ B.WriteMemory(addr2, value);
+ }
+
+ private void PokeSystemBusC(long addr, byte value)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ C.WriteMemory(addr2, value);
+ }
+
+ private void PokeSystemBusD(long addr, byte value)
+ {
+ ushort addr2 = (ushort)(addr & 0xFFFF);
+ D.WriteMemory(addr2, value);
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISaveRam.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISaveRam.cs
new file mode 100644
index 0000000000..689782c57b
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISaveRam.cs
@@ -0,0 +1,125 @@
+using System;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : ISaveRam
+ {
+ public byte[] CloneSaveRam()
+ {
+ if ((A.cart_RAM != null) || (B.cart_RAM != null) || (C.cart_RAM != null) || (D.cart_RAM != null))
+ {
+ int Len1 = 0;
+ int Len2 = 0;
+ int Len3 = 0;
+ int Len4 = 0;
+ int index = 0;
+
+ if (A.cart_RAM != null)
+ {
+ Len1 = A.cart_RAM.Length;
+ }
+
+ if (B.cart_RAM != null)
+ {
+ Len2 = B.cart_RAM.Length;
+ }
+
+ if (C.cart_RAM != null)
+ {
+ Len3 = C.cart_RAM.Length;
+ }
+
+ if (D.cart_RAM != null)
+ {
+ Len4 = D.cart_RAM.Length;
+ }
+
+ byte[] temp = new byte[Len1 + Len2 + Len3 + Len4];
+
+ if (A.cart_RAM != null)
+ {
+ for (int i = 0; i < A.cart_RAM.Length; i++)
+ {
+ temp[index] = A.cart_RAM[i];
+ index++;
+ }
+ }
+
+ if (B.cart_RAM != null)
+ {
+ for (int i = 0; i < B.cart_RAM.Length; i++)
+ {
+ temp[index] = B.cart_RAM[i];
+ index++;
+ }
+ }
+
+ if (C.cart_RAM != null)
+ {
+ for (int i = 0; i < C.cart_RAM.Length; i++)
+ {
+ temp[index] = C.cart_RAM[i];
+ index++;
+ }
+ }
+
+ if (D.cart_RAM != null)
+ {
+ for (int i = 0; i < D.cart_RAM.Length; i++)
+ {
+ temp[index] = D.cart_RAM[i];
+ index++;
+ }
+ }
+
+ return temp;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public void StoreSaveRam(byte[] data)
+ {
+ if (Link4xSyncSettings.Use_SRAM)
+ {
+ int temp = 0;
+
+ if (A.cart_RAM != null)
+ {
+ Buffer.BlockCopy(data, temp, A.cart_RAM, 0, A.cart_RAM.Length);
+ temp += A.cart_RAM.Length;
+ }
+
+ if (B.cart_RAM != null)
+ {
+ Buffer.BlockCopy(data, temp, B.cart_RAM, 0, B.cart_RAM.Length);
+ temp += B.cart_RAM.Length;
+ }
+
+ if (C.cart_RAM != null)
+ {
+ Buffer.BlockCopy(data, temp, C.cart_RAM, 0, C.cart_RAM.Length);
+ temp += C.cart_RAM.Length;
+ }
+
+ if (D.cart_RAM != null)
+ {
+ Buffer.BlockCopy(data, temp, D.cart_RAM, 0, D.cart_RAM.Length);
+ }
+
+ Console.WriteLine("loading SRAM here");
+ }
+ }
+
+ public bool SaveRamModified
+ {
+ get
+ {
+ return (A.has_bat || B.has_bat || C.has_bat || D.has_bat) & Link4xSyncSettings.Use_SRAM;
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs
new file mode 100644
index 0000000000..65239aec31
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs
@@ -0,0 +1,208 @@
+using System;
+using System.ComponentModel;
+
+using Newtonsoft.Json;
+
+using BizHawk.Common;
+using BizHawk.Emulation.Common;
+using BizHawk.Emulation.Cores.Nintendo.GBHawk;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : IEmulator, IStatable, ISettable
+ {
+ public GBLink4xSettings GetSettings()
+ {
+ return Link4xSettings.Clone();
+ }
+
+ public GBLink4xSyncSettings GetSyncSettings()
+ {
+ return Link4xSyncSettings.Clone();
+ }
+
+ public bool PutSettings(GBLink4xSettings o)
+ {
+ Link4xSettings = o;
+ return false;
+ }
+
+ public bool PutSyncSettings(GBLink4xSyncSettings o)
+ {
+ bool ret = GBLink4xSyncSettings.NeedsReboot(Link4xSyncSettings, o);
+ Link4xSyncSettings = o;
+ return ret;
+ }
+
+ private GBLink4xSettings Link4xSettings = new GBLink4xSettings();
+ public GBLink4xSyncSettings Link4xSyncSettings = new GBLink4xSyncSettings();
+
+ public class GBLink4xSettings
+ {
+ [DisplayName("Color Mode")]
+ [Description("Pick Between Green scale and Grey scale colors")]
+ [DefaultValue(GBHawk.GBHawk.GBSettings.PaletteType.BW)]
+ public GBHawk.GBHawk.GBSettings.PaletteType Palette_A { get; set; }
+
+ [DisplayName("Color Mode")]
+ [Description("Pick Between Green scale and Grey scale colors")]
+ [DefaultValue(GBHawk.GBHawk.GBSettings.PaletteType.BW)]
+ public GBHawk.GBHawk.GBSettings.PaletteType Palette_B { get; set; }
+
+ [DisplayName("Color Mode")]
+ [Description("Pick Between Green scale and Grey scale colors")]
+ [DefaultValue(GBHawk.GBHawk.GBSettings.PaletteType.BW)]
+ public GBHawk.GBHawk.GBSettings.PaletteType Palette_C { get; set; }
+
+ [DisplayName("Color Mode")]
+ [Description("Pick Between Green scale and Grey scale colors")]
+ [DefaultValue(GBHawk.GBHawk.GBSettings.PaletteType.BW)]
+ public GBHawk.GBHawk.GBSettings.PaletteType Palette_D { get; set; }
+
+ public enum AudioSrc
+ {
+ A,
+ B,
+ C,
+ D,
+ None
+ }
+
+ [DisplayName("Audio Selection")]
+ [Description("Choose Audio Source. Both will produce Stereo sound.")]
+ [DefaultValue(AudioSrc.A)]
+ public AudioSrc AudioSet { get; set; }
+
+ public GBLink4xSettings Clone()
+ {
+ return (GBLink4xSettings)MemberwiseClone();
+ }
+ }
+
+ public class GBLink4xSyncSettings
+ {
+ [DisplayName("Console Mode A")]
+ [Description("Pick which console to run, 'Auto' chooses from ROM extension, 'GB' and 'GBC' chooses the respective system")]
+ [DefaultValue(GBHawk.GBHawk.GBSyncSettings.ConsoleModeType.Auto)]
+ public GBHawk.GBHawk.GBSyncSettings.ConsoleModeType ConsoleMode_A { get; set; }
+
+ [DisplayName("Console Mode B")]
+ [Description("Pick which console to run, 'Auto' chooses from ROM extension, 'GB' and 'GBC' chooses the respective system")]
+ [DefaultValue(GBHawk.GBHawk.GBSyncSettings.ConsoleModeType.Auto)]
+ public GBHawk.GBHawk.GBSyncSettings.ConsoleModeType ConsoleMode_B { get; set; }
+
+ [DisplayName("Console Mode C")]
+ [Description("Pick which console to run, 'Auto' chooses from ROM extension, 'GB' and 'GBC' chooses the respective system")]
+ [DefaultValue(GBHawk.GBHawk.GBSyncSettings.ConsoleModeType.Auto)]
+ public GBHawk.GBHawk.GBSyncSettings.ConsoleModeType ConsoleMode_C { get; set; }
+
+ [DisplayName("Console Mode D")]
+ [Description("Pick which console to run, 'Auto' chooses from ROM extension, 'GB' and 'GBC' chooses the respective system")]
+ [DefaultValue(GBHawk.GBHawk.GBSyncSettings.ConsoleModeType.Auto)]
+ public GBHawk.GBHawk.GBSyncSettings.ConsoleModeType ConsoleMode_D { get; set; }
+
+ [DisplayName("CGB in GBA")]
+ [Description("Emulate GBA hardware running a CGB game, instead of CGB hardware. Relevant only for titles that detect the presense of a GBA, such as Shantae.")]
+ [DefaultValue(false)]
+ public bool GBACGB { get; set; }
+
+ [DisplayName("RTC Initial Time A")]
+ [Description("Set the initial RTC time in terms of elapsed seconds.")]
+ [DefaultValue(0)]
+ public int RTCInitialTime_A
+ {
+ get { return _RTCInitialTime_A; }
+ set { _RTCInitialTime_A = Math.Max(0, Math.Min(1024 * 24 * 60 * 60, value)); }
+ }
+
+ [DisplayName("RTC Initial Time B")]
+ [Description("Set the initial RTC time in terms of elapsed seconds.")]
+ [DefaultValue(0)]
+ public int RTCInitialTime_B
+ {
+ get { return _RTCInitialTime_B; }
+ set { _RTCInitialTime_B = Math.Max(0, Math.Min(1024 * 24 * 60 * 60, value)); }
+ }
+
+ [DisplayName("RTC Initial Time C")]
+ [Description("Set the initial RTC time in terms of elapsed seconds.")]
+ [DefaultValue(0)]
+ public int RTCInitialTime_C
+ {
+ get { return _RTCInitialTime_C; }
+ set { _RTCInitialTime_C = Math.Max(0, Math.Min(1024 * 24 * 60 * 60, value)); }
+ }
+
+ [DisplayName("RTC Initial Time D")]
+ [Description("Set the initial RTC time in terms of elapsed seconds.")]
+ [DefaultValue(0)]
+ public int RTCInitialTime_D
+ {
+ get { return _RTCInitialTime_D; }
+ set { _RTCInitialTime_D = Math.Max(0, Math.Min(1024 * 24 * 60 * 60, value)); }
+ }
+
+ [DisplayName("Timer Div Initial Time A")]
+ [Description("Don't change from 0 unless it's hardware accurate. GBA GBC mode is known to be 8.")]
+ [DefaultValue(8)]
+ public int DivInitialTime_A
+ {
+ get { return _DivInitialTime_A; }
+ set { _DivInitialTime_A = Math.Min((ushort)65535, (ushort)value); }
+ }
+
+ [DisplayName("Timer Div Initial Time B")]
+ [Description("Don't change from 0 unless it's hardware accurate. GBA GBC mode is known to be 8.")]
+ [DefaultValue(8)]
+ public int DivInitialTime_B
+ {
+ get { return _DivInitialTime_B; }
+ set { _DivInitialTime_B = Math.Min((ushort)65535, (ushort)value); }
+ }
+
+ [DisplayName("Timer Div Initial Time C")]
+ [Description("Don't change from 0 unless it's hardware accurate. GBA GBC mode is known to be 8.")]
+ [DefaultValue(8)]
+ public int DivInitialTime_C
+ {
+ get { return _DivInitialTime_C; }
+ set { _DivInitialTime_C = Math.Min((ushort)65535, (ushort)value); }
+ }
+
+ [DisplayName("Timer Div Initial Time D")]
+ [Description("Don't change from 0 unless it's hardware accurate. GBA GBC mode is known to be 8.")]
+ [DefaultValue(8)]
+ public int DivInitialTime_D
+ {
+ get { return _DivInitialTime_D; }
+ set { _DivInitialTime_D = Math.Min((ushort)65535, (ushort)value); }
+ }
+
+ [DisplayName("Use Existing SaveRAM")]
+ [Description("When true, existing SaveRAM will be loaded at boot up")]
+ [DefaultValue(false)]
+ public bool Use_SRAM { get; set; }
+
+ [JsonIgnore]
+ private int _RTCInitialTime_A;
+ private int _RTCInitialTime_B;
+ private int _RTCInitialTime_C;
+ private int _RTCInitialTime_D;
+ [JsonIgnore]
+ public ushort _DivInitialTime_A = 8;
+ public ushort _DivInitialTime_B = 8;
+ public ushort _DivInitialTime_C = 8;
+ public ushort _DivInitialTime_D = 8;
+
+ public GBLink4xSyncSettings Clone()
+ {
+ return (GBLink4xSyncSettings)MemberwiseClone();
+ }
+
+ public static bool NeedsReboot(GBLink4xSyncSettings x, GBLink4xSyncSettings y)
+ {
+ return !DeepEquality.DeepEquals(x, y);
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IStatable.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IStatable.cs
new file mode 100644
index 0000000000..95ccb8c82e
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.IStatable.cs
@@ -0,0 +1,84 @@
+using System.IO;
+using Newtonsoft.Json;
+
+using BizHawk.Common;
+using BizHawk.Emulation.Common;
+using BizHawk.Emulation.Cores.Nintendo.GBHawk;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public partial class GBHawkLink4x : IStatable
+ {
+ public bool BinarySaveStatesPreferred => true;
+
+ public void SaveStateText(TextWriter writer)
+ {
+ A.SaveStateText(writer);
+ B.SaveStateText(writer);
+ C.SaveStateText(writer);
+ D.SaveStateText(writer);
+ SyncState(new Serializer(writer));
+ }
+
+ public void LoadStateText(TextReader reader)
+ {
+ A.LoadStateText(reader);
+ B.LoadStateText(reader);
+ C.LoadStateText(reader);
+ D.LoadStateText(reader);
+ SyncState(new Serializer(reader));
+ }
+
+ public void SaveStateBinary(BinaryWriter bw)
+ {
+ A.SaveStateBinary(bw);
+ B.SaveStateBinary(bw);
+ C.SaveStateBinary(bw);
+ D.SaveStateBinary(bw);
+ // other variables
+ SyncState(new Serializer(bw));
+ }
+
+ public void LoadStateBinary(BinaryReader br)
+ {
+ A.LoadStateBinary(br);
+ B.LoadStateBinary(br);
+ C.LoadStateBinary(br);
+ D.LoadStateBinary(br);
+ // other variables
+ SyncState(new Serializer(br));
+ }
+
+ public byte[] SaveStateBinary()
+ {
+ MemoryStream ms = new MemoryStream();
+ BinaryWriter bw = new BinaryWriter(ms);
+ SaveStateBinary(bw);
+ bw.Flush();
+ return ms.ToArray();
+ }
+
+ //private JsonSerializer ser = new JsonSerializer { Formatting = Formatting.Indented };
+
+ private void SyncState(Serializer ser)
+ {
+ ser.Sync("Lag", ref _lagcount);
+ ser.Sync("Frame", ref _frame);
+ ser.Sync("IsLag", ref _islag);
+ ser.Sync(nameof(_cableconnected_LC), ref _cableconnected_LC);
+ ser.Sync(nameof(_cableconnected_CR), ref _cableconnected_CR);
+ ser.Sync(nameof(_cableconnected_RL), ref _cableconnected_RL);
+ ser.Sync(nameof(do_2_next), ref do_2_next);
+ ser.Sync(nameof(A_controller), ref A_controller);
+ ser.Sync(nameof(B_controller), ref B_controller);
+ ser.Sync(nameof(C_controller), ref C_controller);
+ ser.Sync(nameof(D_controller), ref D_controller);
+ _controllerDeck.SyncState(ser);
+
+ if (ser.IsReader)
+ {
+ FillVideoBuffer();
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.cs
new file mode 100644
index 0000000000..9c2609a651
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.cs
@@ -0,0 +1,118 @@
+using System;
+
+using BizHawk.Emulation.Common;
+
+using BizHawk.Emulation.Cores.Nintendo.GBHawk;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ [Core(
+ "GBHawkLink4x",
+ "",
+ isPorted: false,
+ isReleased: true)]
+ [ServiceNotApplicable(typeof(IDriveLight))]
+ public partial class GBHawkLink4x : IEmulator, ISaveRam, IDebuggable, IStatable, IInputPollable, IRegionable,
+ ISettable
+ {
+ // we want to create two GBHawk instances that we will run concurrently
+ public GBHawk.GBHawk A;
+ public GBHawk.GBHawk B;
+ public GBHawk.GBHawk C;
+ public GBHawk.GBHawk D;
+
+ // if true, the link cable is currently connected
+ private bool _cableconnected_LC = false;
+ private bool _cableconnected_CR = false;
+ private bool _cableconnected_RL = false;
+
+ private bool do_2_next = false;
+
+ public byte A_controller, B_controller, C_controller, D_controller;
+
+ public bool do_frame_fill;
+
+ //[CoreConstructor("GB", "GBC")]
+ public GBHawkLink4x(CoreComm comm, GameInfo game_A, byte[] rom_A, GameInfo game_B, byte[] rom_B, GameInfo game_C, byte[] rom_C, GameInfo game_D, byte[] rom_D, /*string gameDbFn,*/ object settings, object syncSettings)
+ {
+ var ser = new BasicServiceProvider(this);
+
+ Link4xSettings = (GBLink4xSettings)settings ?? new GBLink4xSettings();
+ Link4xSyncSettings = (GBLink4xSyncSettings)syncSettings ?? new GBLink4xSyncSettings();
+ _controllerDeck = new GBHawkLink4xControllerDeck(GBHawkLink4xControllerDeck.DefaultControllerName, GBHawkLink4xControllerDeck.DefaultControllerName,
+ GBHawkLink4xControllerDeck.DefaultControllerName, GBHawkLink4xControllerDeck.DefaultControllerName);
+
+ CoreComm = comm;
+
+ var temp_set_A = new GBHawk.GBHawk.GBSettings();
+ var temp_set_B = new GBHawk.GBHawk.GBSettings();
+ var temp_set_C = new GBHawk.GBHawk.GBSettings();
+ var temp_set_D = new GBHawk.GBHawk.GBSettings();
+
+ var temp_sync_A = new GBHawk.GBHawk.GBSyncSettings();
+ var temp_sync_B = new GBHawk.GBHawk.GBSyncSettings();
+ var temp_sync_C = new GBHawk.GBHawk.GBSyncSettings();
+ var temp_sync_D = new GBHawk.GBHawk.GBSyncSettings();
+
+ temp_sync_A.ConsoleMode = Link4xSyncSettings.ConsoleMode_A;
+ temp_sync_B.ConsoleMode = Link4xSyncSettings.ConsoleMode_B;
+ temp_sync_C.ConsoleMode = Link4xSyncSettings.ConsoleMode_C;
+ temp_sync_D.ConsoleMode = Link4xSyncSettings.ConsoleMode_D;
+
+ temp_sync_A.DivInitialTime = Link4xSyncSettings.DivInitialTime_A;
+ temp_sync_B.DivInitialTime = Link4xSyncSettings.DivInitialTime_B;
+ temp_sync_C.DivInitialTime = Link4xSyncSettings.DivInitialTime_C;
+ temp_sync_D.DivInitialTime = Link4xSyncSettings.DivInitialTime_D;
+ temp_sync_A.RTCInitialTime = Link4xSyncSettings.RTCInitialTime_A;
+ temp_sync_B.RTCInitialTime = Link4xSyncSettings.RTCInitialTime_B;
+ temp_sync_C.RTCInitialTime = Link4xSyncSettings.RTCInitialTime_C;
+ temp_sync_D.RTCInitialTime = Link4xSyncSettings.RTCInitialTime_D;
+
+ A = new GBHawk.GBHawk(new CoreComm(comm.ShowMessage, comm.Notify) { CoreFileProvider = comm.CoreFileProvider },
+ game_A, rom_A, temp_set_A, temp_sync_A);
+
+ B = new GBHawk.GBHawk(new CoreComm(comm.ShowMessage, comm.Notify) { CoreFileProvider = comm.CoreFileProvider },
+ game_B, rom_B, temp_set_B, temp_sync_B);
+
+ C = new GBHawk.GBHawk(new CoreComm(comm.ShowMessage, comm.Notify) { CoreFileProvider = comm.CoreFileProvider },
+ game_C, rom_C, temp_set_C, temp_sync_C);
+
+ D = new GBHawk.GBHawk(new CoreComm(comm.ShowMessage, comm.Notify) { CoreFileProvider = comm.CoreFileProvider },
+ game_D, rom_D, temp_set_D, temp_sync_D);
+
+ ser.Register(this);
+ ser.Register(this);
+
+ _tracer = new TraceBuffer { Header = A.cpu.TraceHeader };
+ ser.Register(_tracer);
+
+ ServiceProvider = ser;
+
+ SetupMemoryDomains();
+
+ HardReset();
+ }
+
+ public void HardReset()
+ {
+ A.HardReset();
+ B.HardReset();
+ C.HardReset();
+ D.HardReset();
+ }
+
+ public DisplayType Region => DisplayType.NTSC;
+
+ public int _frame = 0;
+
+ private readonly GBHawkLink4xControllerDeck _controllerDeck;
+
+ private readonly ITraceable _tracer;
+
+ private void ExecFetch(ushort addr)
+ {
+ uint flags = (uint)(MemoryCallbackFlags.AccessExecute);
+ MemoryCallbacks.CallMemoryCallbacks(addr, 0, flags, "System Bus");
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4xControllerDeck.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4xControllerDeck.cs
new file mode 100644
index 0000000000..a3be7c5251
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4xControllerDeck.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using BizHawk.Common;
+using BizHawk.Common.ReflectionExtensions;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ public class GBHawkLink4xControllerDeck
+ {
+ public GBHawkLink4xControllerDeck(string controller1Name, string controller2Name, string controller3Name, string controller4Name)
+ {
+ if (!ValidControllerTypes.ContainsKey(controller1Name))
+ {
+ throw new InvalidOperationException("Invalid controller type: " + controller1Name);
+ }
+
+ if (!ValidControllerTypes.ContainsKey(controller2Name))
+ {
+ throw new InvalidOperationException("Invalid controller type: " + controller2Name);
+ }
+
+ if (!ValidControllerTypes.ContainsKey(controller3Name))
+ {
+ throw new InvalidOperationException("Invalid controller type: " + controller3Name);
+ }
+
+ if (!ValidControllerTypes.ContainsKey(controller4Name))
+ {
+ throw new InvalidOperationException("Invalid controller type: " + controller4Name);
+ }
+
+ Port1 = (IPort)Activator.CreateInstance(ValidControllerTypes[controller1Name], 1);
+ Port2 = (IPort)Activator.CreateInstance(ValidControllerTypes[controller2Name], 2);
+ Port3 = (IPort)Activator.CreateInstance(ValidControllerTypes[controller3Name], 3);
+ Port4 = (IPort)Activator.CreateInstance(ValidControllerTypes[controller3Name], 4);
+
+ Definition = new ControllerDefinition
+ {
+ Name = Port1.Definition.Name,
+ BoolButtons = Port1.Definition.BoolButtons
+ .Concat(Port2.Definition.BoolButtons)
+ .Concat(Port3.Definition.BoolButtons)
+ .Concat(Port4.Definition.BoolButtons)
+ .Concat(new[] { "Toggle Cable LC" } )
+ .Concat(new[] { "Toggle Cable CR" } )
+ .Concat(new[] { "Toggle Cable RL" } )
+ .ToList()
+ };
+ }
+
+ public byte ReadPort1(IController c)
+ {
+ return Port1.Read(c);
+ }
+
+ public byte ReadPort2(IController c)
+ {
+ return Port2.Read(c);
+ }
+
+ public byte ReadPort3(IController c)
+ {
+ return Port3.Read(c);
+ }
+
+ public byte ReadPort4(IController c)
+ {
+ return Port4.Read(c);
+ }
+
+ public ControllerDefinition Definition { get; }
+
+ public void SyncState(Serializer ser)
+ {
+ ser.BeginSection(nameof(Port1));
+ Port1.SyncState(ser);
+ ser.EndSection();
+
+ ser.BeginSection(nameof(Port2));
+ Port2.SyncState(ser);
+ ser.EndSection();
+
+ ser.BeginSection(nameof(Port3));
+ Port3.SyncState(ser);
+ ser.EndSection();
+
+ ser.BeginSection(nameof(Port4));
+ Port4.SyncState(ser);
+ ser.EndSection();
+ }
+
+ private readonly IPort Port1;
+ private readonly IPort Port2;
+ private readonly IPort Port3;
+ private readonly IPort Port4;
+
+ private static Dictionary _controllerTypes;
+
+ public static Dictionary ValidControllerTypes
+ {
+ get
+ {
+ if (_controllerTypes == null)
+ {
+ _controllerTypes = typeof(GBHawkLink4xControllerDeck).Assembly
+ .GetTypes()
+ .Where(t => typeof(IPort).IsAssignableFrom(t))
+ .Where(t => !t.IsAbstract && !t.IsInterface)
+ .ToDictionary(tkey => tkey.DisplayName());
+ }
+
+ return _controllerTypes;
+ }
+ }
+
+ public static string DefaultControllerName => typeof(StandardControls).DisplayName();
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4xControllers.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4xControllers.cs
new file mode 100644
index 0000000000..cf0cd408c6
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4xControllers.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+using BizHawk.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
+{
+ ///
+ /// Represents a GB add on
+ ///
+ public interface IPort
+ {
+ byte Read(IController c);
+
+ ControllerDefinition Definition { get; }
+
+ void SyncState(Serializer ser);
+
+ int PortNum { get; }
+ }
+
+ [DisplayName("Gameboy Controller")]
+ public class StandardControls : IPort
+ {
+ public StandardControls(int portNum)
+ {
+ PortNum = portNum;
+ Definition = new ControllerDefinition
+ {
+ Name = "Gameboy Controller H",
+ BoolButtons = BaseDefinition
+ .Select(b => "P" + PortNum + " " + b)
+ .ToList()
+ };
+ }
+
+ public int PortNum { get; }
+
+ public ControllerDefinition Definition { get; }
+
+ public byte Read(IController c)
+ {
+ byte result = 0xFF;
+
+ if (c.IsPressed(Definition.BoolButtons[0]))
+ {
+ result -= 4;
+ }
+ if (c.IsPressed(Definition.BoolButtons[1]))
+ {
+ result -= 8;
+ }
+ if (c.IsPressed(Definition.BoolButtons[2]))
+ {
+ result -= 2;
+ }
+ if (c.IsPressed(Definition.BoolButtons[3]))
+ {
+ result -= 1;
+ }
+ if (c.IsPressed(Definition.BoolButtons[4]))
+ {
+ result -= 128;
+ }
+ if (c.IsPressed(Definition.BoolButtons[5]))
+ {
+ result -= 64;
+ }
+ if (c.IsPressed(Definition.BoolButtons[6]))
+ {
+ result -= 32;
+ }
+ if (c.IsPressed(Definition.BoolButtons[7]))
+ {
+ result -= 16;
+ }
+
+ return result;
+ }
+
+ private static readonly string[] BaseDefinition =
+ {
+ "Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power"
+ };
+
+ public void SyncState(Serializer ser)
+ {
+ //nothing
+ }
+ }
+}
\ No newline at end of file
diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/ReadMe.txt b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/ReadMe.txt
new file mode 100644
index 0000000000..bc60bf4b01
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/ReadMe.txt
@@ -0,0 +1 @@
+TODO: