Fix input config for Gameboy, some small random work on ColecoHawk
This commit is contained in:
parent
5f534518aa
commit
abba87738f
|
@ -10,18 +10,40 @@ namespace BizHawk.Emulation.Consoles.Coleco
|
|||
{
|
||||
public partial class ColecoVision : IEmulator
|
||||
{
|
||||
public byte[] rom;
|
||||
public byte[] rom = new byte[2048];
|
||||
public byte[] expansion = new byte[0x4000];
|
||||
public byte[] cartridgeslot = new byte[0xFFFF]; //TODO: how big should this be?
|
||||
public Z80A cpu;
|
||||
public VDP Vdp; //adelikat: Using the SMS one for now
|
||||
|
||||
public byte ReadMemory(ushort addr)
|
||||
{
|
||||
return 0xFF;
|
||||
if (addr < 0x2000)
|
||||
{
|
||||
return rom[addr];
|
||||
}
|
||||
else if (addr >= 0x2000 && addr < 0x6000)
|
||||
{
|
||||
return expansion[addr];
|
||||
}
|
||||
else if (addr >= 0x6000 && addr < 0x8000)
|
||||
{
|
||||
return ram[addr & 1023];
|
||||
}
|
||||
else if (addr >= 0x8000)
|
||||
{
|
||||
return cartridgeslot[addr];
|
||||
}
|
||||
|
||||
else return 0xFF;
|
||||
}
|
||||
|
||||
public void WriteMemory(ushort addr, byte value)
|
||||
{
|
||||
return;
|
||||
if (addr >= 0x6000 && addr < 0x8000)
|
||||
{
|
||||
ram[addr] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void HardReset()
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace BizHawk.Emulation.Consoles.Coleco
|
|||
public CoreOutputComm CoreOutputComm { get; private set; }
|
||||
public IVideoProvider VideoProvider { get { return this; } }
|
||||
public ISoundProvider SoundProvider { get { return this; } }
|
||||
public byte[] ram = new byte[1024];
|
||||
public byte[] ram = new byte[2048];
|
||||
|
||||
public DisplayType DisplayType { get; set; } //TOOD: delete me
|
||||
|
||||
|
|
|
@ -5,516 +5,521 @@ using BizHawk.Emulation.CPUs.Z80;
|
|||
|
||||
namespace BizHawk.Emulation.Consoles.Coleco
|
||||
{
|
||||
public enum VdpMode { SMS, GameGear } //TODO: delete me
|
||||
public enum VdpMode { SMS, GameGear } //TODO: delete me
|
||||
|
||||
// Emulates the Texas Instruments TMS9918 VDP.
|
||||
public sealed partial class VDP : IVideoProvider
|
||||
{
|
||||
// VDP State
|
||||
public byte[] VRAM = new byte[0x4000]; //16kb video RAM
|
||||
public byte[] CRAM; // SMS = 32 bytes, GG = 64 bytes CRAM
|
||||
public byte[] Registers = new byte[] { 0x06, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xF0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
public byte StatusByte;
|
||||
// Emulates the Texas Instruments TMS9918 VDP.
|
||||
public sealed partial class VDP : IVideoProvider
|
||||
{
|
||||
// VDP State
|
||||
public byte[] VRAM = new byte[0x4000]; //16kb video RAM
|
||||
public byte[] CRAM; // SMS = 32 bytes, GG = 64 bytes CRAM
|
||||
public byte[] Registers = new byte[] { 0x06, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xF0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
public byte StatusByte;
|
||||
|
||||
bool VdpWaitingForLatchByte = true;
|
||||
byte VdpLatch;
|
||||
byte VdpBuffer;
|
||||
ushort VdpAddress;
|
||||
VdpCommand vdpCommand;
|
||||
int TmsMode = 4;
|
||||
bool VdpWaitingForLatchByte = true;
|
||||
byte VdpLatch;
|
||||
byte VdpBuffer;
|
||||
ushort VdpAddress;
|
||||
VdpCommand vdpCommand;
|
||||
int TmsMode = 4;
|
||||
|
||||
bool VIntPending;
|
||||
bool HIntPending;
|
||||
bool VIntPending;
|
||||
bool HIntPending;
|
||||
|
||||
ColecoVision Sms; //TODO: rename
|
||||
VdpMode mode;
|
||||
DisplayType DisplayType = DisplayType.NTSC;
|
||||
Z80A Cpu;
|
||||
ColecoVision Coleco;
|
||||
VdpMode mode;
|
||||
DisplayType DisplayType = DisplayType.NTSC;
|
||||
Z80A Cpu;
|
||||
|
||||
public bool SpriteLimit;
|
||||
public int IPeriod = 228;
|
||||
public VdpMode VdpMode { get { return mode; } }
|
||||
public bool SpriteLimit;
|
||||
public int IPeriod = 228;
|
||||
public VdpMode VdpMode { get { return mode; } }
|
||||
|
||||
int FrameHeight = 192;
|
||||
public int ScanLine;
|
||||
public int[] FrameBuffer = new int[256*192];
|
||||
public int[] GameGearFrameBuffer = new int[160*144];
|
||||
int FrameHeight = 192;
|
||||
public int ScanLine;
|
||||
public int[] FrameBuffer = new int[256 * 192];
|
||||
public int[] GameGearFrameBuffer = new int[160 * 144];
|
||||
|
||||
public bool Mode1Bit { get { return (Registers[1] & 16) > 0;} }
|
||||
public bool Mode2Bit { get { return (Registers[0] & 2) > 0; } }
|
||||
public bool Mode3Bit { get { return (Registers[1] & 8) > 0; } }
|
||||
public bool Mode4Bit { get { return (Registers[0] & 4) > 0; } }
|
||||
public bool ShiftSpritesLeft8Pixels { get { return (Registers[0] & 8) > 0; } }
|
||||
public bool EnableLineInterrupts { get { return (Registers[0] & 16) > 0; } }
|
||||
public bool LeftBlanking { get { return (Registers[0] & 32) > 0; } }
|
||||
public bool HorizScrollLock { get { return (Registers[0] & 64) > 0; } }
|
||||
public bool VerticalScrollLock { get { return (Registers[0] & 128) > 0; } }
|
||||
public bool EnableDoubledSprites { get { return (Registers[1] & 1) > 0; } }
|
||||
public bool EnableLargeSprites { get { return (Registers[1] & 2) > 0; } }
|
||||
public bool EnableFrameInterrupts { get { return (Registers[1] & 32) > 0; } }
|
||||
public bool DisplayOn { get { return (Registers[1] & 64) > 0; } }
|
||||
public int SpriteAttributeTableBase { get { return ((Registers[5] >> 1) << 8) & 0x3FFF; } }
|
||||
public int SpriteTileBase { get { return (Registers[6] & 4) > 0 ? 256: 0; } }
|
||||
public byte BackdropColor { get { return (byte)(16 + (Registers[7] & 15)); } }
|
||||
public bool Mode1Bit { get { return (Registers[1] & 16) > 0; } }
|
||||
public bool Mode2Bit { get { return (Registers[0] & 2) > 0; } }
|
||||
public bool Mode3Bit { get { return (Registers[1] & 8) > 0; } }
|
||||
public bool Mode4Bit { get { return (Registers[0] & 4) > 0; } }
|
||||
public bool ShiftSpritesLeft8Pixels { get { return (Registers[0] & 8) > 0; } }
|
||||
public bool EnableLineInterrupts { get { return (Registers[0] & 16) > 0; } }
|
||||
public bool LeftBlanking { get { return (Registers[0] & 32) > 0; } }
|
||||
public bool HorizScrollLock { get { return (Registers[0] & 64) > 0; } }
|
||||
public bool VerticalScrollLock { get { return (Registers[0] & 128) > 0; } }
|
||||
public bool EnableDoubledSprites { get { return (Registers[1] & 1) > 0; } }
|
||||
public bool EnableLargeSprites { get { return (Registers[1] & 2) > 0; } }
|
||||
public bool EnableFrameInterrupts { get { return (Registers[1] & 32) > 0; } }
|
||||
public bool DisplayOn { get { return (Registers[1] & 64) > 0; } }
|
||||
public int SpriteAttributeTableBase { get { return ((Registers[5] >> 1) << 8) & 0x3FFF; } }
|
||||
public int SpriteTileBase { get { return (Registers[6] & 4) > 0 ? 256 : 0; } }
|
||||
public byte BackdropColor { get { return (byte)(16 + (Registers[7] & 15)); } }
|
||||
|
||||
int NameTableBase;
|
||||
int ColorTableBase;
|
||||
int PatternGeneratorBase;
|
||||
int SpritePatternGeneratorBase;
|
||||
int TmsPatternNameTableBase;
|
||||
int TmsSpriteAttributeBase;
|
||||
|
||||
// preprocessed state assist stuff.
|
||||
public int[] Palette = new int[32];
|
||||
public byte[] PatternBuffer = new byte[0x8000];
|
||||
int NameTableBase;
|
||||
int ColorTableBase;
|
||||
int PatternGeneratorBase;
|
||||
int SpritePatternGeneratorBase;
|
||||
int TmsPatternNameTableBase;
|
||||
int TmsSpriteAttributeBase;
|
||||
|
||||
byte[] ScanlinePriorityBuffer = new byte[256];
|
||||
byte[] SpriteCollisionBuffer = new byte[256];
|
||||
// preprocessed state assist stuff.
|
||||
public int[] Palette = new int[32];
|
||||
public byte[] PatternBuffer = new byte[0x8000];
|
||||
|
||||
static readonly byte[] SMSPalXlatTable = { 0, 85, 170, 255 };
|
||||
static readonly byte[] GGPalXlatTable = { 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 };
|
||||
byte[] ScanlinePriorityBuffer = new byte[256];
|
||||
byte[] SpriteCollisionBuffer = new byte[256];
|
||||
|
||||
public VDP(ColecoVision sms, Z80A cpu, VdpMode mode, DisplayType displayType)
|
||||
{
|
||||
Sms = sms;
|
||||
Cpu = cpu;
|
||||
this.mode = mode;
|
||||
if (mode == VdpMode.SMS) CRAM = new byte[32];
|
||||
if (mode == VdpMode.GameGear) CRAM = new byte[64];
|
||||
DisplayType = displayType;
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
static readonly byte[] SMSPalXlatTable = { 0, 85, 170, 255 };
|
||||
static readonly byte[] GGPalXlatTable = { 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 };
|
||||
|
||||
public byte ReadData()
|
||||
{
|
||||
VdpWaitingForLatchByte = true;
|
||||
byte value = VdpBuffer;
|
||||
VdpBuffer = VRAM[VdpAddress & 0x3FFF];
|
||||
VdpAddress++;
|
||||
return value;
|
||||
}
|
||||
public VDP(ColecoVision sms, Z80A cpu, VdpMode mode, DisplayType displayType)
|
||||
{
|
||||
Coleco = sms;
|
||||
Cpu = cpu;
|
||||
this.mode = mode;
|
||||
if (mode == VdpMode.SMS) CRAM = new byte[32];
|
||||
if (mode == VdpMode.GameGear) CRAM = new byte[64];
|
||||
DisplayType = displayType;
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
|
||||
public byte ReadVdpStatus()
|
||||
{
|
||||
VdpWaitingForLatchByte = true;
|
||||
byte returnValue = StatusByte;
|
||||
StatusByte &= 0x1F;
|
||||
HIntPending = false;
|
||||
VIntPending = false;
|
||||
Cpu.Interrupt = false;
|
||||
return returnValue;
|
||||
}
|
||||
public byte ReadData()
|
||||
{
|
||||
VdpWaitingForLatchByte = true;
|
||||
byte value = VdpBuffer;
|
||||
VdpBuffer = VRAM[VdpAddress & 0x3FFF];
|
||||
VdpAddress++;
|
||||
return value;
|
||||
}
|
||||
|
||||
public byte ReadVLineCounter()
|
||||
{
|
||||
if (DisplayType == DisplayType.NTSC)
|
||||
{
|
||||
if (FrameHeight == 192)
|
||||
return VLineCounterTableNTSC192[ScanLine];
|
||||
if (FrameHeight == 224)
|
||||
return VLineCounterTableNTSC224[ScanLine];
|
||||
return VLineCounterTableNTSC240[ScanLine];
|
||||
} else { // PAL
|
||||
if (FrameHeight == 192)
|
||||
return VLineCounterTablePAL192[ScanLine];
|
||||
if (FrameHeight == 224)
|
||||
return VLineCounterTablePAL224[ScanLine];
|
||||
return VLineCounterTablePAL240[ScanLine];
|
||||
}
|
||||
}
|
||||
public byte ReadVdpStatus()
|
||||
{
|
||||
VdpWaitingForLatchByte = true;
|
||||
byte returnValue = StatusByte;
|
||||
StatusByte &= 0x1F;
|
||||
HIntPending = false;
|
||||
VIntPending = false;
|
||||
Cpu.Interrupt = false;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public void WriteVdpControl(byte value)
|
||||
{
|
||||
if (VdpWaitingForLatchByte)
|
||||
{
|
||||
VdpLatch = value;
|
||||
VdpWaitingForLatchByte = false;
|
||||
VdpAddress = (ushort)((VdpAddress & 0xFF00) | value);
|
||||
return;
|
||||
}
|
||||
public byte ReadVLineCounter()
|
||||
{
|
||||
if (DisplayType == DisplayType.NTSC)
|
||||
{
|
||||
if (FrameHeight == 192)
|
||||
return VLineCounterTableNTSC192[ScanLine];
|
||||
if (FrameHeight == 224)
|
||||
return VLineCounterTableNTSC224[ScanLine];
|
||||
return VLineCounterTableNTSC240[ScanLine];
|
||||
}
|
||||
else
|
||||
{ // PAL
|
||||
if (FrameHeight == 192)
|
||||
return VLineCounterTablePAL192[ScanLine];
|
||||
if (FrameHeight == 224)
|
||||
return VLineCounterTablePAL224[ScanLine];
|
||||
return VLineCounterTablePAL240[ScanLine];
|
||||
}
|
||||
}
|
||||
|
||||
VdpWaitingForLatchByte = true;
|
||||
VdpAddress = (ushort)(((value & 63) << 8) | VdpLatch);
|
||||
switch (value & 0xC0)
|
||||
{
|
||||
case 0x00: // read VRAM
|
||||
vdpCommand = VdpCommand.VramRead;
|
||||
VdpBuffer = VRAM[VdpAddress & 0x3FFF];
|
||||
VdpAddress++;
|
||||
break;
|
||||
case 0x40: // write VRAM
|
||||
vdpCommand = VdpCommand.VramWrite;
|
||||
break;
|
||||
case 0x80: // VDP register write
|
||||
vdpCommand = VdpCommand.RegisterWrite;
|
||||
int reg = value & 0x0F;
|
||||
WriteRegister(reg, VdpLatch);
|
||||
break;
|
||||
case 0xC0: // write CRAM / modify palette
|
||||
vdpCommand = VdpCommand.CramWrite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
public void WriteVdpControl(byte value)
|
||||
{
|
||||
if (VdpWaitingForLatchByte)
|
||||
{
|
||||
VdpLatch = value;
|
||||
VdpWaitingForLatchByte = false;
|
||||
VdpAddress = (ushort)((VdpAddress & 0xFF00) | value);
|
||||
return;
|
||||
}
|
||||
|
||||
public void WriteVdpData(byte value)
|
||||
{
|
||||
VdpWaitingForLatchByte = true;
|
||||
VdpBuffer = value;
|
||||
if (vdpCommand == VdpCommand.CramWrite)
|
||||
{
|
||||
// Write Palette / CRAM
|
||||
int mask = VdpMode == VdpMode.SMS ? 0x1F : 0x3F;
|
||||
CRAM[VdpAddress & mask] = value;
|
||||
UpdatePrecomputedPalette();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write VRAM and update pre-computed pattern buffer.
|
||||
UpdatePatternBuffer((ushort)(VdpAddress & 0x3FFF), value);
|
||||
VRAM[VdpAddress & 0x3FFF] = value;
|
||||
}
|
||||
VdpAddress++;
|
||||
}
|
||||
VdpWaitingForLatchByte = true;
|
||||
VdpAddress = (ushort)(((value & 63) << 8) | VdpLatch);
|
||||
switch (value & 0xC0)
|
||||
{
|
||||
case 0x00: // read VRAM
|
||||
vdpCommand = VdpCommand.VramRead;
|
||||
VdpBuffer = VRAM[VdpAddress & 0x3FFF];
|
||||
VdpAddress++;
|
||||
break;
|
||||
case 0x40: // write VRAM
|
||||
vdpCommand = VdpCommand.VramWrite;
|
||||
break;
|
||||
case 0x80: // VDP register write
|
||||
vdpCommand = VdpCommand.RegisterWrite;
|
||||
int reg = value & 0x0F;
|
||||
WriteRegister(reg, VdpLatch);
|
||||
break;
|
||||
case 0xC0: // write CRAM / modify palette
|
||||
vdpCommand = VdpCommand.CramWrite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePrecomputedPalette()
|
||||
{
|
||||
if (mode == VdpMode.SMS)
|
||||
{
|
||||
for (int i=0; i<32; i++)
|
||||
{
|
||||
byte value = CRAM[i];
|
||||
byte r = SMSPalXlatTable[(value & 0x03)];
|
||||
byte g = SMSPalXlatTable[(value & 0x0C) >> 2];
|
||||
byte b = SMSPalXlatTable[(value & 0x30) >> 4];
|
||||
Palette[i] = Colors.ARGB(r, g, b);
|
||||
}
|
||||
} else { // GameGear
|
||||
for (int i=0; i<32; i++)
|
||||
{
|
||||
ushort value = (ushort) ((CRAM[(i*2) + 1] << 8) | CRAM[(i*2) + 0]);
|
||||
byte r = GGPalXlatTable[(value & 0x000F)];
|
||||
byte g = GGPalXlatTable[(value & 0x00F0) >> 4];
|
||||
byte b = GGPalXlatTable[(value & 0x0F00) >> 8];
|
||||
Palette[i] = Colors.ARGB(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void WriteVdpData(byte value)
|
||||
{
|
||||
VdpWaitingForLatchByte = true;
|
||||
VdpBuffer = value;
|
||||
if (vdpCommand == VdpCommand.CramWrite)
|
||||
{
|
||||
// Write Palette / CRAM
|
||||
int mask = VdpMode == VdpMode.SMS ? 0x1F : 0x3F;
|
||||
CRAM[VdpAddress & mask] = value;
|
||||
UpdatePrecomputedPalette();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write VRAM and update pre-computed pattern buffer.
|
||||
UpdatePatternBuffer((ushort)(VdpAddress & 0x3FFF), value);
|
||||
VRAM[VdpAddress & 0x3FFF] = value;
|
||||
}
|
||||
VdpAddress++;
|
||||
}
|
||||
|
||||
public int CalcNameTableBase()
|
||||
{
|
||||
if (FrameHeight == 192)
|
||||
return 1024 * (Registers[2] & 0x0E);
|
||||
return (1024 * (Registers[2] & 0x0C)) + 0x0700;
|
||||
}
|
||||
public void UpdatePrecomputedPalette()
|
||||
{
|
||||
if (mode == VdpMode.SMS)
|
||||
{
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
byte value = CRAM[i];
|
||||
byte r = SMSPalXlatTable[(value & 0x03)];
|
||||
byte g = SMSPalXlatTable[(value & 0x0C) >> 2];
|
||||
byte b = SMSPalXlatTable[(value & 0x30) >> 4];
|
||||
Palette[i] = Colors.ARGB(r, g, b);
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // GameGear
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
ushort value = (ushort)((CRAM[(i * 2) + 1] << 8) | CRAM[(i * 2) + 0]);
|
||||
byte r = GGPalXlatTable[(value & 0x000F)];
|
||||
byte g = GGPalXlatTable[(value & 0x00F0) >> 4];
|
||||
byte b = GGPalXlatTable[(value & 0x0F00) >> 8];
|
||||
Palette[i] = Colors.ARGB(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckVideoMode()
|
||||
{
|
||||
if (Mode4Bit == false) // check old TMS modes
|
||||
{
|
||||
if (Mode1Bit) TmsMode = 1;
|
||||
else if (Mode2Bit) TmsMode = 2;
|
||||
else if (Mode3Bit) TmsMode = 3;
|
||||
else TmsMode = 0;
|
||||
}
|
||||
public int CalcNameTableBase()
|
||||
{
|
||||
if (FrameHeight == 192)
|
||||
return 1024 * (Registers[2] & 0x0E);
|
||||
return (1024 * (Registers[2] & 0x0C)) + 0x0700;
|
||||
}
|
||||
|
||||
else if (Mode4Bit && Mode2Bit) // if Mode4 and Mode2 set, then check extension modes
|
||||
{
|
||||
TmsMode = 4;
|
||||
switch (Registers[1] & 0x18)
|
||||
{
|
||||
case 0x00:
|
||||
case 0x18: // 192-line mode
|
||||
if (FrameHeight != 192)
|
||||
{
|
||||
FrameHeight = 192;
|
||||
FrameBuffer = new int[256*192];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
break;
|
||||
case 0x10: // 224-line mode
|
||||
if (FrameHeight != 224)
|
||||
{
|
||||
FrameHeight = 224;
|
||||
FrameBuffer = new int[256*224];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
break;
|
||||
case 0x08: // 240-line mode
|
||||
if (FrameHeight != 240)
|
||||
{
|
||||
FrameHeight = 240;
|
||||
FrameBuffer = new int[256 * 240];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
else { // default to standard 192-line mode4
|
||||
TmsMode = 4;
|
||||
if (FrameHeight != 192)
|
||||
{
|
||||
FrameHeight = 192;
|
||||
FrameBuffer = new int[256*192];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
}
|
||||
}
|
||||
void CheckVideoMode()
|
||||
{
|
||||
if (Mode4Bit == false) // check old TMS modes
|
||||
{
|
||||
if (Mode1Bit) TmsMode = 1;
|
||||
else if (Mode2Bit) TmsMode = 2;
|
||||
else if (Mode3Bit) TmsMode = 3;
|
||||
else TmsMode = 0;
|
||||
}
|
||||
|
||||
void WriteRegister(int reg, byte data)
|
||||
{
|
||||
Registers[reg] = data;
|
||||
switch(reg)
|
||||
{
|
||||
case 0: // Mode Control Register 1
|
||||
CheckVideoMode();
|
||||
Cpu.Interrupt = (EnableLineInterrupts && HIntPending);
|
||||
break;
|
||||
case 1: // Mode Control Register 2
|
||||
CheckVideoMode();
|
||||
Cpu.Interrupt = (EnableFrameInterrupts && VIntPending);
|
||||
break;
|
||||
case 2: // Name Table Base Address
|
||||
NameTableBase = CalcNameTableBase();
|
||||
TmsPatternNameTableBase = (Registers[2] << 10) & 0x3C00;
|
||||
break;
|
||||
case 3: // Color Table Base Address
|
||||
ColorTableBase = (Registers[3] << 6) & 0x3FC0;
|
||||
break;
|
||||
case 4: // Pattern Generator Base Address
|
||||
PatternGeneratorBase = (Registers[4] << 11) & 0x3800;
|
||||
break;
|
||||
case 5: // Sprite Attribute Table Base Address
|
||||
// ??? should I move from my property to precalculated?
|
||||
TmsSpriteAttributeBase = (Registers[5] << 7) & 0x3F80;
|
||||
break;
|
||||
case 6: // Sprite Pattern Generator Base Adderss
|
||||
SpritePatternGeneratorBase = (Registers[6] << 11) & 0x3800;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (Mode4Bit && Mode2Bit) // if Mode4 and Mode2 set, then check extension modes
|
||||
{
|
||||
TmsMode = 4;
|
||||
switch (Registers[1] & 0x18)
|
||||
{
|
||||
case 0x00:
|
||||
case 0x18: // 192-line mode
|
||||
if (FrameHeight != 192)
|
||||
{
|
||||
FrameHeight = 192;
|
||||
FrameBuffer = new int[256 * 192];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
break;
|
||||
case 0x10: // 224-line mode
|
||||
if (FrameHeight != 224)
|
||||
{
|
||||
FrameHeight = 224;
|
||||
FrameBuffer = new int[256 * 224];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
break;
|
||||
case 0x08: // 240-line mode
|
||||
if (FrameHeight != 240)
|
||||
{
|
||||
FrameHeight = 240;
|
||||
FrameBuffer = new int[256 * 240];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static readonly byte[] pow2 = {1, 2, 4, 8, 16, 32, 64, 128};
|
||||
|
||||
void UpdatePatternBuffer(ushort address, byte value)
|
||||
{
|
||||
// writing one byte affects 8 pixels due to stupid planar storage.
|
||||
for (int i=0; i<8; i++)
|
||||
{
|
||||
byte colorBit = pow2[address%4];
|
||||
byte sourceBit = pow2[7 - i];
|
||||
ushort dest = (ushort) (((address & 0xFFFC)*2) + i);
|
||||
if ((value & sourceBit) > 0) // setting bit
|
||||
PatternBuffer[dest] |= colorBit;
|
||||
else // clearing bit
|
||||
PatternBuffer[dest] &= (byte)~colorBit;
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // default to standard 192-line mode4
|
||||
TmsMode = 4;
|
||||
if (FrameHeight != 192)
|
||||
{
|
||||
FrameHeight = 192;
|
||||
FrameBuffer = new int[256 * 192];
|
||||
NameTableBase = CalcNameTableBase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int lineIntLinesRemaining;
|
||||
void WriteRegister(int reg, byte data)
|
||||
{
|
||||
Registers[reg] = data;
|
||||
switch (reg)
|
||||
{
|
||||
case 0: // Mode Control Register 1
|
||||
CheckVideoMode();
|
||||
Cpu.Interrupt = (EnableLineInterrupts && HIntPending);
|
||||
break;
|
||||
case 1: // Mode Control Register 2
|
||||
CheckVideoMode();
|
||||
Cpu.Interrupt = (EnableFrameInterrupts && VIntPending);
|
||||
break;
|
||||
case 2: // Name Table Base Address
|
||||
NameTableBase = CalcNameTableBase();
|
||||
TmsPatternNameTableBase = (Registers[2] << 10) & 0x3C00;
|
||||
break;
|
||||
case 3: // Color Table Base Address
|
||||
ColorTableBase = (Registers[3] << 6) & 0x3FC0;
|
||||
break;
|
||||
case 4: // Pattern Generator Base Address
|
||||
PatternGeneratorBase = (Registers[4] << 11) & 0x3800;
|
||||
break;
|
||||
case 5: // Sprite Attribute Table Base Address
|
||||
// ??? should I move from my property to precalculated?
|
||||
TmsSpriteAttributeBase = (Registers[5] << 7) & 0x3F80;
|
||||
break;
|
||||
case 6: // Sprite Pattern Generator Base Adderss
|
||||
SpritePatternGeneratorBase = (Registers[6] << 11) & 0x3800;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessFrameInterrupt()
|
||||
{
|
||||
if (ScanLine == FrameHeight + 1)
|
||||
{
|
||||
StatusByte |= 0x80;
|
||||
VIntPending = true;
|
||||
}
|
||||
static readonly byte[] pow2 = { 1, 2, 4, 8, 16, 32, 64, 128 };
|
||||
|
||||
if (VIntPending && EnableFrameInterrupts)
|
||||
Cpu.Interrupt = true;
|
||||
}
|
||||
void UpdatePatternBuffer(ushort address, byte value)
|
||||
{
|
||||
// writing one byte affects 8 pixels due to stupid planar storage.
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
byte colorBit = pow2[address % 4];
|
||||
byte sourceBit = pow2[7 - i];
|
||||
ushort dest = (ushort)(((address & 0xFFFC) * 2) + i);
|
||||
if ((value & sourceBit) > 0) // setting bit
|
||||
PatternBuffer[dest] |= colorBit;
|
||||
else // clearing bit
|
||||
PatternBuffer[dest] &= (byte)~colorBit;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessLineInterrupt()
|
||||
{
|
||||
if (ScanLine <= FrameHeight)
|
||||
{
|
||||
if (lineIntLinesRemaining-- <= 0)
|
||||
{
|
||||
HIntPending = true;
|
||||
if (EnableLineInterrupts)
|
||||
Cpu.Interrupt = true;
|
||||
lineIntLinesRemaining = Registers[0x0A];
|
||||
}
|
||||
return;
|
||||
}
|
||||
// else we're outside the active display period
|
||||
lineIntLinesRemaining = Registers[0x0A];
|
||||
}
|
||||
int lineIntLinesRemaining;
|
||||
|
||||
public void ExecFrame(bool render)
|
||||
{
|
||||
int scanlinesPerFrame = DisplayType == DisplayType.NTSC ? 262 : 313;
|
||||
for (ScanLine = 0; ScanLine < scanlinesPerFrame; ScanLine++)
|
||||
{
|
||||
RenderCurrentScanline(render);
|
||||
void ProcessFrameInterrupt()
|
||||
{
|
||||
if (ScanLine == FrameHeight + 1)
|
||||
{
|
||||
StatusByte |= 0x80;
|
||||
VIntPending = true;
|
||||
}
|
||||
|
||||
ProcessFrameInterrupt();
|
||||
ProcessLineInterrupt();
|
||||
if (VIntPending && EnableFrameInterrupts)
|
||||
Cpu.Interrupt = true;
|
||||
}
|
||||
|
||||
Cpu.ExecuteCycles(IPeriod);
|
||||
void ProcessLineInterrupt()
|
||||
{
|
||||
if (ScanLine <= FrameHeight)
|
||||
{
|
||||
if (lineIntLinesRemaining-- <= 0)
|
||||
{
|
||||
HIntPending = true;
|
||||
if (EnableLineInterrupts)
|
||||
Cpu.Interrupt = true;
|
||||
lineIntLinesRemaining = Registers[0x0A];
|
||||
}
|
||||
return;
|
||||
}
|
||||
// else we're outside the active display period
|
||||
lineIntLinesRemaining = Registers[0x0A];
|
||||
}
|
||||
|
||||
if (ScanLine == scanlinesPerFrame - 1)
|
||||
RenderBlankingRegions();
|
||||
}
|
||||
}
|
||||
public void ExecFrame(bool render)
|
||||
{
|
||||
int scanlinesPerFrame = DisplayType == DisplayType.NTSC ? 262 : 313;
|
||||
for (ScanLine = 0; ScanLine < scanlinesPerFrame; ScanLine++)
|
||||
{
|
||||
RenderCurrentScanline(render);
|
||||
|
||||
internal void RenderCurrentScanline(bool render)
|
||||
{
|
||||
if (ScanLine >= FrameHeight)
|
||||
return;
|
||||
ProcessFrameInterrupt();
|
||||
ProcessLineInterrupt();
|
||||
|
||||
if (TmsMode == 4)
|
||||
{
|
||||
if (render == false)
|
||||
{
|
||||
ProcessSpriteCollisionForFrameskip();
|
||||
return;
|
||||
}
|
||||
Cpu.ExecuteCycles(IPeriod);
|
||||
|
||||
RenderBackgroundCurrentLine(Sms.CoreInputComm.SMS_ShowBG);
|
||||
if (ScanLine == scanlinesPerFrame - 1)
|
||||
RenderBlankingRegions();
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableDoubledSprites)
|
||||
RenderSpritesCurrentLineDoubleSize(Sms.CoreInputComm.SMS_ShowOBJ);
|
||||
else
|
||||
RenderSpritesCurrentLine(Sms.CoreInputComm.SMS_ShowOBJ);
|
||||
}
|
||||
else if (TmsMode == 2)
|
||||
{
|
||||
if (render == false)
|
||||
return;
|
||||
internal void RenderCurrentScanline(bool render)
|
||||
{
|
||||
if (ScanLine >= FrameHeight)
|
||||
return;
|
||||
|
||||
RenderBackgroundM2(Sms.CoreInputComm.SMS_ShowBG);
|
||||
RenderTmsSprites(Sms.CoreInputComm.SMS_ShowOBJ);
|
||||
}
|
||||
else if (TmsMode == 0)
|
||||
{
|
||||
if (render == false)
|
||||
return;
|
||||
if (TmsMode == 4)
|
||||
{
|
||||
if (render == false)
|
||||
{
|
||||
ProcessSpriteCollisionForFrameskip();
|
||||
return;
|
||||
}
|
||||
|
||||
RenderBackgroundM0(Sms.CoreInputComm.SMS_ShowBG);
|
||||
RenderTmsSprites(Sms.CoreInputComm.SMS_ShowOBJ);
|
||||
}
|
||||
}
|
||||
RenderBackgroundCurrentLine(Coleco.CoreInputComm.SMS_ShowBG);
|
||||
|
||||
public void SaveStateText(TextWriter writer)
|
||||
{
|
||||
writer.WriteLine("[VDP]");
|
||||
writer.WriteLine("StatusByte {0:X2}", StatusByte);
|
||||
writer.WriteLine("WaitingForLatchByte {0}", VdpWaitingForLatchByte);
|
||||
writer.WriteLine("Latch {0:X2}", VdpLatch);
|
||||
writer.WriteLine("ReadBuffer {0:X2}", VdpBuffer);
|
||||
writer.WriteLine("VdpAddress {0:X4}", VdpAddress);
|
||||
writer.WriteLine("Command " + Enum.GetName(typeof(VdpCommand), vdpCommand));
|
||||
if (EnableDoubledSprites)
|
||||
RenderSpritesCurrentLineDoubleSize(Coleco.CoreInputComm.SMS_ShowOBJ);
|
||||
else
|
||||
RenderSpritesCurrentLine(Coleco.CoreInputComm.SMS_ShowOBJ);
|
||||
}
|
||||
else if (TmsMode == 2)
|
||||
{
|
||||
if (render == false)
|
||||
return;
|
||||
|
||||
writer.Write("Registers ");
|
||||
Registers.SaveAsHex(writer);
|
||||
writer.Write("CRAM ");
|
||||
CRAM.SaveAsHex(writer);
|
||||
writer.Write("VRAM ");
|
||||
VRAM.SaveAsHex(writer);
|
||||
RenderBackgroundM2(Coleco.CoreInputComm.SMS_ShowBG);
|
||||
RenderTmsSprites(Coleco.CoreInputComm.SMS_ShowOBJ);
|
||||
}
|
||||
else if (TmsMode == 0)
|
||||
{
|
||||
if (render == false)
|
||||
return;
|
||||
|
||||
writer.WriteLine("[/VDP]");
|
||||
writer.WriteLine();
|
||||
}
|
||||
RenderBackgroundM0(Coleco.CoreInputComm.SMS_ShowBG);
|
||||
RenderTmsSprites(Coleco.CoreInputComm.SMS_ShowOBJ);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadStateText(TextReader reader)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string[] args = reader.ReadLine().Split(' ');
|
||||
if (args[0].Trim() == "") continue;
|
||||
if (args[0] == "[/VDP]") break;
|
||||
if (args[0] == "StatusByte")
|
||||
StatusByte = byte.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "WaitingForLatchByte")
|
||||
VdpWaitingForLatchByte = bool.Parse(args[1]);
|
||||
else if (args[0] == "Latch")
|
||||
VdpLatch = byte.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "ReadBuffer")
|
||||
VdpBuffer = byte.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "VdpAddress")
|
||||
VdpAddress = ushort.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "Command")
|
||||
vdpCommand = (VdpCommand) Enum.Parse(typeof (VdpCommand), args[1]);
|
||||
else if (args[0] == "Registers")
|
||||
Registers.ReadFromHex(args[1]);
|
||||
else if (args[0] == "CRAM")
|
||||
{
|
||||
CRAM.ReadFromHex(args[1]);
|
||||
UpdatePrecomputedPalette();
|
||||
}
|
||||
else if (args[0] == "VRAM")
|
||||
{
|
||||
VRAM.ReadFromHex(args[1]);
|
||||
for (ushort i=0; i<VRAM.Length; i++)
|
||||
UpdatePatternBuffer(i, VRAM[i]);
|
||||
}
|
||||
public void SaveStateText(TextWriter writer)
|
||||
{
|
||||
writer.WriteLine("[VDP]");
|
||||
writer.WriteLine("StatusByte {0:X2}", StatusByte);
|
||||
writer.WriteLine("WaitingForLatchByte {0}", VdpWaitingForLatchByte);
|
||||
writer.WriteLine("Latch {0:X2}", VdpLatch);
|
||||
writer.WriteLine("ReadBuffer {0:X2}", VdpBuffer);
|
||||
writer.WriteLine("VdpAddress {0:X4}", VdpAddress);
|
||||
writer.WriteLine("Command " + Enum.GetName(typeof(VdpCommand), vdpCommand));
|
||||
|
||||
else
|
||||
Console.WriteLine("Skipping unrecognized identifier "+args[0]);
|
||||
}
|
||||
for (int i=0; i<Registers.Length; i++)
|
||||
WriteRegister(i, Registers[i]);
|
||||
}
|
||||
writer.Write("Registers ");
|
||||
Registers.SaveAsHex(writer);
|
||||
writer.Write("CRAM ");
|
||||
CRAM.SaveAsHex(writer);
|
||||
writer.Write("VRAM ");
|
||||
VRAM.SaveAsHex(writer);
|
||||
|
||||
public void SaveStateBinary(BinaryWriter writer)
|
||||
{
|
||||
writer.Write(StatusByte);
|
||||
writer.Write(VdpWaitingForLatchByte);
|
||||
writer.Write(VdpLatch);
|
||||
writer.Write(VdpBuffer);
|
||||
writer.Write(VdpAddress);
|
||||
writer.Write((byte)vdpCommand);
|
||||
writer.Write(Registers);
|
||||
writer.Write(CRAM);
|
||||
writer.Write(VRAM);
|
||||
}
|
||||
writer.WriteLine("[/VDP]");
|
||||
writer.WriteLine();
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader reader)
|
||||
{
|
||||
StatusByte = reader.ReadByte();
|
||||
VdpWaitingForLatchByte = reader.ReadBoolean();
|
||||
VdpLatch = reader.ReadByte();
|
||||
VdpBuffer = reader.ReadByte();
|
||||
VdpAddress = reader.ReadUInt16();
|
||||
vdpCommand = (VdpCommand) Enum.ToObject(typeof(VdpCommand), reader.ReadByte());
|
||||
Registers = reader.ReadBytes(Registers.Length);
|
||||
CRAM = reader.ReadBytes(CRAM.Length);
|
||||
VRAM = reader.ReadBytes(VRAM.Length);
|
||||
UpdatePrecomputedPalette();
|
||||
for (ushort i = 0; i < VRAM.Length; i++)
|
||||
UpdatePatternBuffer(i, VRAM[i]);
|
||||
for (int i = 0; i < Registers.Length; i++)
|
||||
WriteRegister(i, Registers[i]);
|
||||
}
|
||||
public void LoadStateText(TextReader reader)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string[] args = reader.ReadLine().Split(' ');
|
||||
if (args[0].Trim() == "") continue;
|
||||
if (args[0] == "[/VDP]") break;
|
||||
if (args[0] == "StatusByte")
|
||||
StatusByte = byte.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "WaitingForLatchByte")
|
||||
VdpWaitingForLatchByte = bool.Parse(args[1]);
|
||||
else if (args[0] == "Latch")
|
||||
VdpLatch = byte.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "ReadBuffer")
|
||||
VdpBuffer = byte.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "VdpAddress")
|
||||
VdpAddress = ushort.Parse(args[1], NumberStyles.HexNumber);
|
||||
else if (args[0] == "Command")
|
||||
vdpCommand = (VdpCommand)Enum.Parse(typeof(VdpCommand), args[1]);
|
||||
else if (args[0] == "Registers")
|
||||
Registers.ReadFromHex(args[1]);
|
||||
else if (args[0] == "CRAM")
|
||||
{
|
||||
CRAM.ReadFromHex(args[1]);
|
||||
UpdatePrecomputedPalette();
|
||||
}
|
||||
else if (args[0] == "VRAM")
|
||||
{
|
||||
VRAM.ReadFromHex(args[1]);
|
||||
for (ushort i = 0; i < VRAM.Length; i++)
|
||||
UpdatePatternBuffer(i, VRAM[i]);
|
||||
}
|
||||
|
||||
public int[] GetVideoBuffer()
|
||||
{
|
||||
return mode == VdpMode.SMS ? FrameBuffer : GameGearFrameBuffer;
|
||||
}
|
||||
else
|
||||
Console.WriteLine("Skipping unrecognized identifier " + args[0]);
|
||||
}
|
||||
for (int i = 0; i < Registers.Length; i++)
|
||||
WriteRegister(i, Registers[i]);
|
||||
}
|
||||
|
||||
public int BufferWidth
|
||||
{
|
||||
get { return mode == VdpMode.SMS ? 256 : 160; }
|
||||
}
|
||||
public void SaveStateBinary(BinaryWriter writer)
|
||||
{
|
||||
writer.Write(StatusByte);
|
||||
writer.Write(VdpWaitingForLatchByte);
|
||||
writer.Write(VdpLatch);
|
||||
writer.Write(VdpBuffer);
|
||||
writer.Write(VdpAddress);
|
||||
writer.Write((byte)vdpCommand);
|
||||
writer.Write(Registers);
|
||||
writer.Write(CRAM);
|
||||
writer.Write(VRAM);
|
||||
}
|
||||
|
||||
public int BufferHeight
|
||||
{
|
||||
get { return mode == VdpMode.SMS ? FrameHeight : 144; }
|
||||
}
|
||||
public void LoadStateBinary(BinaryReader reader)
|
||||
{
|
||||
StatusByte = reader.ReadByte();
|
||||
VdpWaitingForLatchByte = reader.ReadBoolean();
|
||||
VdpLatch = reader.ReadByte();
|
||||
VdpBuffer = reader.ReadByte();
|
||||
VdpAddress = reader.ReadUInt16();
|
||||
vdpCommand = (VdpCommand)Enum.ToObject(typeof(VdpCommand), reader.ReadByte());
|
||||
Registers = reader.ReadBytes(Registers.Length);
|
||||
CRAM = reader.ReadBytes(CRAM.Length);
|
||||
VRAM = reader.ReadBytes(VRAM.Length);
|
||||
UpdatePrecomputedPalette();
|
||||
for (ushort i = 0; i < VRAM.Length; i++)
|
||||
UpdatePatternBuffer(i, VRAM[i]);
|
||||
for (int i = 0; i < Registers.Length; i++)
|
||||
WriteRegister(i, Registers[i]);
|
||||
}
|
||||
|
||||
public int BackgroundColor
|
||||
{
|
||||
get { return Palette[BackdropColor]; }
|
||||
}
|
||||
public int[] GetVideoBuffer()
|
||||
{
|
||||
return mode == VdpMode.SMS ? FrameBuffer : GameGearFrameBuffer;
|
||||
}
|
||||
|
||||
enum VdpCommand
|
||||
{
|
||||
VramRead,
|
||||
VramWrite,
|
||||
RegisterWrite,
|
||||
CramWrite
|
||||
}
|
||||
}
|
||||
public int BufferWidth
|
||||
{
|
||||
get { return mode == VdpMode.SMS ? 256 : 160; }
|
||||
}
|
||||
|
||||
public int BufferHeight
|
||||
{
|
||||
get { return mode == VdpMode.SMS ? FrameHeight : 144; }
|
||||
}
|
||||
|
||||
public int BackgroundColor
|
||||
{
|
||||
get { return Palette[BackdropColor]; }
|
||||
}
|
||||
|
||||
enum VdpCommand
|
||||
{
|
||||
VramRead,
|
||||
VramWrite,
|
||||
RegisterWrite,
|
||||
CramWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,6 +159,8 @@ namespace BizHawk.MultiClient
|
|||
IDX_CONTROLLERENABLED.Checked = ((Atari2600ControllerTemplate)mainController[jpad]).Enabled;
|
||||
break;
|
||||
case "Gameboy":
|
||||
IDX_CONTROLLERENABLED.Checked = ((GBControllerTemplate)mainController[jpad]).Enabled;
|
||||
break;
|
||||
case "NES":
|
||||
IDX_CONTROLLERENABLED.Checked = ((NESControllerTemplate)mainController[jpad]).Enabled;
|
||||
break;
|
||||
|
@ -206,6 +208,11 @@ namespace BizHawk.MultiClient
|
|||
break;
|
||||
}
|
||||
case "Gameboy":
|
||||
{
|
||||
GBControllerTemplate obj = (GBControllerTemplate)controller[jpad];
|
||||
field = obj.GetType().GetField(fieldName).GetValue(obj);
|
||||
break;
|
||||
}
|
||||
case "NES":
|
||||
{
|
||||
NESControllerTemplate obj = (NESControllerTemplate)controller[jpad];
|
||||
|
@ -319,6 +326,8 @@ namespace BizHawk.MultiClient
|
|||
((Atari2600ControllerTemplate)mainController[prev]).Enabled = IDX_CONTROLLERENABLED.Checked;
|
||||
break;
|
||||
case "Gameboy":
|
||||
((GBControllerTemplate)mainController[prev]).Enabled = IDX_CONTROLLERENABLED.Checked;
|
||||
break;
|
||||
case "NES":
|
||||
((NESControllerTemplate)mainController[prev]).Enabled = IDX_CONTROLLERENABLED.Checked;
|
||||
break;
|
||||
|
@ -357,6 +366,13 @@ namespace BizHawk.MultiClient
|
|||
break;
|
||||
}
|
||||
case "Gameboy":
|
||||
{
|
||||
GBControllerTemplate obj = (GBControllerTemplate)controller[prev];
|
||||
FieldInfo buttonField = obj.GetType().GetField(fieldName);
|
||||
field = buttonField.GetValue(obj);
|
||||
buttonField.SetValue(obj, AppendButtonMapping(TempBox.Text, (string)field));
|
||||
break;
|
||||
}
|
||||
case "NES":
|
||||
{
|
||||
NESControllerTemplate obj = (NESControllerTemplate)controller[prev];
|
||||
|
|
Loading…
Reference in New Issue