Updated VDP to handle GameGear. Added Turbo mode. Versio 1.0 me thinks!
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace Core.Video
|
namespace Core.Video
|
||||||
{
|
{
|
||||||
public class SmsVdp
|
public class SmsVdp
|
||||||
{
|
{
|
||||||
public bool IsPalRegion { get; set; } = false;
|
|
||||||
// The VDP's private memory! The CPU cannot touch these arrays directly.
|
// The VDP's private memory! The CPU cannot touch these arrays directly.
|
||||||
public byte[] VRAM { get; private set; } = new byte[0x4000]; // 16KB Video RAM
|
public byte[] VRAM { get; private set; } = new byte[0x4000]; // 16KB Video RAM
|
||||||
public byte[] CRAM { get; private set; } = new byte[0x20]; // 32 Bytes Color Palette
|
public byte[] smsCRAM { get; private set; } = new byte[0x20]; // Master System - 32 Bytes colour Palette
|
||||||
|
public byte[] ggCRAM { get; private set; } = new byte[0x40]; // GameGear - 64 Bytes colour palette
|
||||||
public byte[] Registers { get; private set; } = new byte[16]; // 11 Hardware Control Registers
|
public byte[] Registers { get; private set; } = new byte[16]; // 11 Hardware Control Registers
|
||||||
public int[] FrameBuffer { get; private set; } = new int[256 * 192];
|
public int[] FrameBuffer { get; private set; } = new int[256 * 192];
|
||||||
private bool[] _priorityBuffer = new bool[256 * 192]; // Tracks priority pixels!
|
private bool[] _priorityBuffer = new bool[256 * 192]; // Tracks priority pixels!
|
||||||
@@ -26,11 +27,14 @@ namespace Core.Video
|
|||||||
private int _currentScanline = 0;
|
private int _currentScanline = 0;
|
||||||
private int _lineCounter = 0;
|
private int _lineCounter = 0;
|
||||||
private byte _statusRegister = 0x00;
|
private byte _statusRegister = 0x00;
|
||||||
|
public bool IsGameGear { get; set; } = false;
|
||||||
|
|
||||||
public bool InterruptPending =>
|
public bool InterruptPending =>
|
||||||
((_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0) || // VBlank
|
((_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0) || // VBlank
|
||||||
((_statusRegister & 0x40) != 0 && (Registers[0] & 0x10) != 0); // Line Interrupt
|
((_statusRegister & 0x40) != 0 && (Registers[0] & 0x10) != 0); // Line Interrupt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public byte ReadDataPort() // Port 0xBE
|
public byte ReadDataPort() // Port 0xBE
|
||||||
{
|
{
|
||||||
_isSecondControlByte = false; // Reading data resets the control latch
|
_isSecondControlByte = false; // Reading data resets the control latch
|
||||||
@@ -45,8 +49,8 @@ namespace Core.Video
|
|||||||
_isSecondControlByte = false;
|
_isSecondControlByte = false;
|
||||||
byte currentStatus = _statusRegister;
|
byte currentStatus = _statusRegister;
|
||||||
|
|
||||||
// CRITICAL HARDWARE QUIRK: Reading the status port physically
|
// Reading the status port physically
|
||||||
// clears the flags inside the chip! If we don't clear this,
|
// clears the flags inside the chip If we don't clear this,
|
||||||
// the interrupt line gets stuck on forever.
|
// the interrupt line gets stuck on forever.
|
||||||
_statusRegister = 0x00;
|
_statusRegister = 0x00;
|
||||||
|
|
||||||
@@ -61,15 +65,24 @@ namespace Core.Video
|
|||||||
int address = _controlWord & 0x3FFF;
|
int address = _controlWord & 0x3FFF;
|
||||||
int command = (_controlWord >> 14) & 0x03;
|
int command = (_controlWord >> 14) & 0x03;
|
||||||
|
|
||||||
if (command == 3) // Code 3: Write to Color Palette (CRAM)
|
if (command == 3) // Code 3: Write to Colour Palette (CRAM)
|
||||||
{
|
{
|
||||||
CRAM[address & 0x1F] = value;
|
if (IsGameGear)
|
||||||
|
{
|
||||||
|
ggCRAM[address & 0x3F] = value; // GG has 64 bytes of CRAM
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
smsCRAM[address & 0x1F] = value; // SMS has 32 bytes of CRAM
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else // Code 0, 1, 2: Write to VRAM
|
else // THE FIX: Code 0, 1, 2: Write graphics to VRAM!
|
||||||
{
|
{
|
||||||
VRAM[address] = value;
|
VRAM[address] = value;
|
||||||
}
|
}
|
||||||
_controlWord++; // Auto-increment so the Z80 can blast data fast
|
|
||||||
|
// THE FIX: The pointer MUST auto-increment so the CPU can blast data fast!
|
||||||
|
_controlWord++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteControlPort(byte value) // Port 0xBF
|
public void WriteControlPort(byte value) // Port 0xBF
|
||||||
@@ -105,23 +118,14 @@ namespace Core.Video
|
|||||||
|
|
||||||
public byte ReadVCounter()
|
public byte ReadVCounter()
|
||||||
{
|
{
|
||||||
if (IsPalRegion)
|
// NTSC Math: 262 lines. Counts 0 to 218, jumps to 213 (0xD5), counts to 255.
|
||||||
{
|
|
||||||
// PAL Math: 313 lines. Counts 0 to 242, jumps to 186 (0xBA), counts to 255.
|
|
||||||
if (_currentScanline <= 242) return (byte)_currentScanline;
|
|
||||||
else return (byte)(_currentScanline - 57);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// NTSC Math: 262 lines. Counts 0 to 218, jumps to 213 (0xD5), counts to 255.
|
|
||||||
if (_currentScanline <= 218) return (byte)_currentScanline;
|
if (_currentScanline <= 218) return (byte)_currentScanline;
|
||||||
else return (byte)(_currentScanline - 6);
|
else return (byte)(_currentScanline - 6);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public byte ReadHCounter()
|
public byte ReadHCounter()
|
||||||
{
|
{
|
||||||
// The Master System H-Counter is a notoriously weird 8-bit timer.
|
// The Master System H-Counter counts from 0x00 to 0x93, then jumps forward to 0xE9, ending at 0xFF.
|
||||||
// It counts from 0x00 to 0x93, then jumps forward to 0xE9, ending at 0xFF.
|
|
||||||
|
|
||||||
// 1 T-State = 1.5 pixels. The H-Counter increments every 2 pixels.
|
// 1 T-State = 1.5 pixels. The H-Counter increments every 2 pixels.
|
||||||
// So H = T * 0.75
|
// So H = T * 0.75
|
||||||
@@ -173,7 +177,7 @@ namespace Core.Video
|
|||||||
// 3. MOVE TO THE NEXT LINE
|
// 3. MOVE TO THE NEXT LINE
|
||||||
_currentScanline++;
|
_currentScanline++;
|
||||||
|
|
||||||
int maxLines = IsPalRegion ? 313 : 262;
|
int maxLines = 262;
|
||||||
|
|
||||||
if (_currentScanline > maxLines -1)
|
if (_currentScanline > maxLines -1)
|
||||||
{
|
{
|
||||||
@@ -197,7 +201,6 @@ namespace Core.Video
|
|||||||
for (int x = 0; x < 256; x++) FrameBuffer[(screenY * 256) + x] = unchecked((int)0xFF000000);
|
for (int x = 0; x < 256; x++) FrameBuffer[(screenY * 256) + x] = unchecked((int)0xFF000000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 1. RENDER BACKGROUND LINE ---
|
// --- 1. RENDER BACKGROUND LINE ---
|
||||||
ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10);
|
ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10);
|
||||||
int scrollX = Registers[8];
|
int scrollX = Registers[8];
|
||||||
@@ -213,16 +216,10 @@ namespace Core.Video
|
|||||||
// --- LEFT COLUMN MASKING (OVERSCAN CURTAIN) ---
|
// --- LEFT COLUMN MASKING (OVERSCAN CURTAIN) ---
|
||||||
if (maskLeftCol && screenX < 8)
|
if (maskLeftCol && screenX < 8)
|
||||||
{
|
{
|
||||||
// Draw the physical backdrop color (from Sprite Palette + Reg 7 index)
|
// REPLACE THE R/G/B MATH WITH THIS SINGLE LINE:
|
||||||
byte bgSmsColor = CRAM[16 + (Registers[7] & 0x0F)];
|
|
||||||
int bgR = (bgSmsColor & 0x03) * 85;
|
|
||||||
int bgG = ((bgSmsColor >> 2) & 0x03) * 85;
|
|
||||||
int bgB = ((bgSmsColor >> 4) & 0x03) * 85;
|
|
||||||
|
|
||||||
int bgAddress = (screenY * 256) + screenX;
|
int bgAddress = (screenY * 256) + screenX;
|
||||||
FrameBuffer[bgAddress] = (255 << 24) | (bgR << 16) | (bgG << 8) | bgB;
|
FrameBuffer[bgAddress] = GetRgbColour(16, Registers[7] & 0x0F);
|
||||||
|
|
||||||
// Flag it as priority so sprites also hide behind the curtain!
|
|
||||||
_priorityBuffer[bgAddress] = true;
|
_priorityBuffer[bgAddress] = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -261,21 +258,17 @@ namespace Core.Video
|
|||||||
byte bp3 = VRAM[tileAddress + (readY * 4) + 3];
|
byte bp3 = VRAM[tileAddress + (readY * 4) + 3];
|
||||||
|
|
||||||
int readX = flipH ? tileX : (7 - tileX);
|
int readX = flipH ? tileX : (7 - tileX);
|
||||||
int colorIndex = ((bp0 >> readX) & 1) | (((bp1 >> readX) & 1) << 1) |
|
int colourIndex = ((bp0 >> readX) & 1) | (((bp1 >> readX) & 1) << 1) |
|
||||||
(((bp2 >> readX) & 1) << 2) | (((bp3 >> readX) & 1) << 3);
|
(((bp2 >> readX) & 1) << 2) | (((bp3 >> readX) & 1) << 3);
|
||||||
|
|
||||||
int paletteOffset = useSpritePalette ? 16 : 0;
|
int paletteOffset = useSpritePalette ? 16 : 0;
|
||||||
byte smsColor = CRAM[paletteOffset + colorIndex];
|
int finalColour = GetRgbColour(paletteOffset, colourIndex);
|
||||||
|
|
||||||
int r = (smsColor & 0x03) * 85;
|
|
||||||
int g = ((smsColor >> 2) & 0x03) * 85;
|
|
||||||
int b = ((smsColor >> 4) & 0x03) * 85;
|
|
||||||
|
|
||||||
int screenAddress = (screenY * 256) + screenX;
|
int screenAddress = (screenY * 256) + screenX;
|
||||||
|
|
||||||
// Draw background and reset priority mask for this exact pixel
|
// Draw background and reset priority mask for this exact pixel
|
||||||
FrameBuffer[screenAddress] = (255 << 24) | (r << 16) | (g << 8) | b;
|
FrameBuffer[screenAddress] = finalColour;
|
||||||
_priorityBuffer[screenAddress] = (priority && colorIndex != 0);
|
_priorityBuffer[screenAddress] = (priority && colourIndex != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. RENDER SPRITE LINE ---
|
// --- 2. RENDER SPRITE LINE ---
|
||||||
@@ -329,24 +322,53 @@ namespace Core.Video
|
|||||||
if (_priorityBuffer[(screenY * 256) + screenX]) continue;
|
if (_priorityBuffer[(screenY * 256) + screenX]) continue;
|
||||||
|
|
||||||
int shift = 7 - px;
|
int shift = 7 - px;
|
||||||
int colorIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) |
|
int colourIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) |
|
||||||
(((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3);
|
(((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3);
|
||||||
|
|
||||||
if (colorIndex == 0) continue;
|
if (colourIndex == 0) continue;
|
||||||
|
|
||||||
byte smsColor = CRAM[16 + colorIndex];
|
// REPLACE THE R/G/B MATH WITH THIS SINGLE LINE:
|
||||||
int r = (smsColor & 0x03) * 85;
|
FrameBuffer[(screenY * 256) + screenX] = GetRgbColour(16, colourIndex);
|
||||||
int g = ((smsColor >> 2) & 0x03) * 85;
|
|
||||||
int b = ((smsColor >> 4) & 0x03) * 85;
|
|
||||||
|
|
||||||
FrameBuffer[(screenY * 256) + screenX] = (255 << 24) | (r << 16) | (g << 8) | b;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int GetRgbColour(int paletteOffset, int colourIndex)
|
||||||
|
{
|
||||||
|
//Debug.WriteLine(_isGameGear);
|
||||||
|
int r, g, b;
|
||||||
|
|
||||||
|
if (IsGameGear)
|
||||||
|
{
|
||||||
|
// Game Gear: 2 bytes per colour. Format: ----BBBBGGGGRRRR
|
||||||
|
int cramIndex = (paletteOffset + colourIndex) * 2;
|
||||||
|
ushort ggcolour = (ushort)(ggCRAM[cramIndex] | (ggCRAM[cramIndex + 1] << 8));
|
||||||
|
|
||||||
|
// Extract 4-bit values (0-15) and map them to standard 8-bit values (0-255).
|
||||||
|
// We multiply by 17 because 255 / 15 = 17!
|
||||||
|
r = (ggcolour & 0x0F) * 17;
|
||||||
|
g = ((ggcolour >> 4) & 0x0F) * 17;
|
||||||
|
b = ((ggcolour >> 8) & 0x0F) * 17;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Master System: 1 byte per colour. Format: --BBGGRR
|
||||||
|
byte smscolour = smsCRAM[paletteOffset + colourIndex];
|
||||||
|
|
||||||
|
// Extract 2-bit values (0-3) and map them to standard 8-bit values (0-255).
|
||||||
|
// We multiply by 85 because 255 / 3 = 85!
|
||||||
|
r = (smscolour & 0x03) * 85;
|
||||||
|
g = ((smscolour >> 2) & 0x03) * 85;
|
||||||
|
b = ((smscolour >> 4) & 0x03) * 85;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (255 << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
public void SaveState(BinaryWriter bw)
|
public void SaveState(BinaryWriter bw)
|
||||||
{
|
{
|
||||||
bw.Write(VRAM);
|
bw.Write(VRAM);
|
||||||
bw.Write(CRAM);
|
bw.Write(smsCRAM);
|
||||||
|
bw.Write(ggCRAM);
|
||||||
bw.Write(Registers);
|
bw.Write(Registers);
|
||||||
bw.Write(_isSecondControlByte);
|
bw.Write(_isSecondControlByte);
|
||||||
bw.Write(_controlWord);
|
bw.Write(_controlWord);
|
||||||
@@ -359,12 +381,14 @@ namespace Core.Video
|
|||||||
// ADD THESE:
|
// ADD THESE:
|
||||||
bw.Write(_latchedHScroll);
|
bw.Write(_latchedHScroll);
|
||||||
bw.Write(_latchedVScroll);
|
bw.Write(_latchedVScroll);
|
||||||
|
bw.Write(IsGameGear);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadState(BinaryReader br)
|
public void LoadState(BinaryReader br)
|
||||||
{
|
{
|
||||||
Array.Copy(br.ReadBytes(VRAM.Length), VRAM, VRAM.Length);
|
Array.Copy(br.ReadBytes(VRAM.Length), VRAM, VRAM.Length);
|
||||||
Array.Copy(br.ReadBytes(CRAM.Length), CRAM, CRAM.Length);
|
Array.Copy(br.ReadBytes(smsCRAM.Length), smsCRAM, smsCRAM.Length);
|
||||||
|
Array.Copy(br.ReadBytes(ggCRAM.Length), ggCRAM, ggCRAM.Length);
|
||||||
Array.Copy(br.ReadBytes(Registers.Length), Registers, Registers.Length);
|
Array.Copy(br.ReadBytes(Registers.Length), Registers, Registers.Length);
|
||||||
_isSecondControlByte = br.ReadBoolean();
|
_isSecondControlByte = br.ReadBoolean();
|
||||||
_controlWord = br.ReadUInt16();
|
_controlWord = br.ReadUInt16();
|
||||||
@@ -377,6 +401,7 @@ namespace Core.Video
|
|||||||
// ADD THESE:
|
// ADD THESE:
|
||||||
_latchedHScroll = br.ReadInt32();
|
_latchedHScroll = br.ReadInt32();
|
||||||
_latchedVScroll = br.ReadInt32();
|
_latchedVScroll = br.ReadInt32();
|
||||||
|
IsGameGear = br.ReadBoolean();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
41
Desktop/Form1.Designer.cs
generated
41
Desktop/Form1.Designer.cs
generated
@@ -41,9 +41,11 @@
|
|||||||
resetToolStripMenuItem = new ToolStripMenuItem();
|
resetToolStripMenuItem = new ToolStripMenuItem();
|
||||||
saveStateToolStripMenuItem = new ToolStripMenuItem();
|
saveStateToolStripMenuItem = new ToolStripMenuItem();
|
||||||
loadStateToolStripMenuItem = new ToolStripMenuItem();
|
loadStateToolStripMenuItem = new ToolStripMenuItem();
|
||||||
|
turboModeToolStripMenuItem = new ToolStripMenuItem();
|
||||||
helpToolStripMenuItem = new ToolStripMenuItem();
|
helpToolStripMenuItem = new ToolStripMenuItem();
|
||||||
aboutToolStripMenuItem = new ToolStripMenuItem();
|
aboutToolStripMenuItem = new ToolStripMenuItem();
|
||||||
pALRegionToolStripMenuItem = new ToolStripMenuItem();
|
masterSystemToolStripMenuItem = new ToolStripMenuItem();
|
||||||
|
gameGearToolStripMenuItem = new ToolStripMenuItem();
|
||||||
menuStrip1.SuspendLayout();
|
menuStrip1.SuspendLayout();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
@@ -74,15 +76,15 @@
|
|||||||
//
|
//
|
||||||
// includedToolStripMenuItem
|
// includedToolStripMenuItem
|
||||||
//
|
//
|
||||||
|
includedToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { masterSystemToolStripMenuItem, gameGearToolStripMenuItem });
|
||||||
includedToolStripMenuItem.Name = "includedToolStripMenuItem";
|
includedToolStripMenuItem.Name = "includedToolStripMenuItem";
|
||||||
includedToolStripMenuItem.Size = new Size(178, 26);
|
includedToolStripMenuItem.Size = new Size(224, 26);
|
||||||
includedToolStripMenuItem.Text = "Included";
|
includedToolStripMenuItem.Text = "Included";
|
||||||
includedToolStripMenuItem.Click += includedToolStripMenuItem_Click;
|
|
||||||
//
|
//
|
||||||
// selectROMToolStripMenuItem1
|
// selectROMToolStripMenuItem1
|
||||||
//
|
//
|
||||||
selectROMToolStripMenuItem1.Name = "selectROMToolStripMenuItem1";
|
selectROMToolStripMenuItem1.Name = "selectROMToolStripMenuItem1";
|
||||||
selectROMToolStripMenuItem1.Size = new Size(178, 26);
|
selectROMToolStripMenuItem1.Size = new Size(224, 26);
|
||||||
selectROMToolStripMenuItem1.Text = "Select ROM...";
|
selectROMToolStripMenuItem1.Text = "Select ROM...";
|
||||||
selectROMToolStripMenuItem1.Click += selectROMToolStripMenuItem_Click;
|
selectROMToolStripMenuItem1.Click += selectROMToolStripMenuItem_Click;
|
||||||
//
|
//
|
||||||
@@ -116,7 +118,7 @@
|
|||||||
//
|
//
|
||||||
// machineToolStripMenuItem
|
// machineToolStripMenuItem
|
||||||
//
|
//
|
||||||
machineToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { resetToolStripMenuItem, saveStateToolStripMenuItem, loadStateToolStripMenuItem, pALRegionToolStripMenuItem });
|
machineToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { resetToolStripMenuItem, saveStateToolStripMenuItem, loadStateToolStripMenuItem, turboModeToolStripMenuItem });
|
||||||
machineToolStripMenuItem.Name = "machineToolStripMenuItem";
|
machineToolStripMenuItem.Name = "machineToolStripMenuItem";
|
||||||
machineToolStripMenuItem.Size = new Size(79, 24);
|
machineToolStripMenuItem.Size = new Size(79, 24);
|
||||||
machineToolStripMenuItem.Text = "Machine";
|
machineToolStripMenuItem.Text = "Machine";
|
||||||
@@ -142,6 +144,13 @@
|
|||||||
loadStateToolStripMenuItem.Text = "Load State";
|
loadStateToolStripMenuItem.Text = "Load State";
|
||||||
loadStateToolStripMenuItem.Click += loadStateToolStripMenuItem_Click;
|
loadStateToolStripMenuItem.Click += loadStateToolStripMenuItem_Click;
|
||||||
//
|
//
|
||||||
|
// turboModeToolStripMenuItem
|
||||||
|
//
|
||||||
|
turboModeToolStripMenuItem.Name = "turboModeToolStripMenuItem";
|
||||||
|
turboModeToolStripMenuItem.Size = new Size(224, 26);
|
||||||
|
turboModeToolStripMenuItem.Text = "Turbo Mode";
|
||||||
|
turboModeToolStripMenuItem.Click += turboModeToolStripMenuItem_Click;
|
||||||
|
//
|
||||||
// helpToolStripMenuItem
|
// helpToolStripMenuItem
|
||||||
//
|
//
|
||||||
helpToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { aboutToolStripMenuItem });
|
helpToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { aboutToolStripMenuItem });
|
||||||
@@ -155,15 +164,17 @@
|
|||||||
aboutToolStripMenuItem.Size = new Size(133, 26);
|
aboutToolStripMenuItem.Size = new Size(133, 26);
|
||||||
aboutToolStripMenuItem.Text = "About";
|
aboutToolStripMenuItem.Text = "About";
|
||||||
//
|
//
|
||||||
// pALRegionToolStripMenuItem
|
// masterSystemToolStripMenuItem
|
||||||
//
|
//
|
||||||
pALRegionToolStripMenuItem.Checked = true;
|
masterSystemToolStripMenuItem.Name = "masterSystemToolStripMenuItem";
|
||||||
pALRegionToolStripMenuItem.CheckOnClick = true;
|
masterSystemToolStripMenuItem.Size = new Size(224, 26);
|
||||||
pALRegionToolStripMenuItem.CheckState = CheckState.Checked;
|
masterSystemToolStripMenuItem.Text = "Master System";
|
||||||
pALRegionToolStripMenuItem.Name = "pALRegionToolStripMenuItem";
|
//
|
||||||
pALRegionToolStripMenuItem.Size = new Size(224, 26);
|
// gameGearToolStripMenuItem
|
||||||
pALRegionToolStripMenuItem.Text = "PAL Region?";
|
//
|
||||||
pALRegionToolStripMenuItem.Click += pALRegionToolStripMenuItem_Click;
|
gameGearToolStripMenuItem.Name = "gameGearToolStripMenuItem";
|
||||||
|
gameGearToolStripMenuItem.Size = new Size(224, 26);
|
||||||
|
gameGearToolStripMenuItem.Text = "Game Gear";
|
||||||
//
|
//
|
||||||
// ParsonsForm1
|
// ParsonsForm1
|
||||||
//
|
//
|
||||||
@@ -197,6 +208,8 @@
|
|||||||
private ToolStripMenuItem vRAMViewerToolStripMenuItem;
|
private ToolStripMenuItem vRAMViewerToolStripMenuItem;
|
||||||
private ToolStripMenuItem saveStateToolStripMenuItem;
|
private ToolStripMenuItem saveStateToolStripMenuItem;
|
||||||
private ToolStripMenuItem loadStateToolStripMenuItem;
|
private ToolStripMenuItem loadStateToolStripMenuItem;
|
||||||
private ToolStripMenuItem pALRegionToolStripMenuItem;
|
private ToolStripMenuItem turboModeToolStripMenuItem;
|
||||||
|
private ToolStripMenuItem masterSystemToolStripMenuItem;
|
||||||
|
private ToolStripMenuItem gameGearToolStripMenuItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
141
Desktop/Form1.cs
141
Desktop/Form1.cs
@@ -18,14 +18,14 @@ namespace Desktop
|
|||||||
private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb);
|
private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb);
|
||||||
private NAudioPlayer _audioPlayer;
|
private NAudioPlayer _audioPlayer;
|
||||||
private Task _emulatorTask;
|
private Task _emulatorTask;
|
||||||
private double TargetFrameTime = 16.667f; //NTSC
|
|
||||||
//private double TargetFrameTime = 20; //PAL
|
|
||||||
public int TotalFrameCount = 0;
|
public int TotalFrameCount = 0;
|
||||||
public double FrameTime { get; private set; } = 0;
|
public double FrameTime { get; private set; } = 0;
|
||||||
public double FramesPerSecond { get; private set; } = 0;
|
public double FramesPerSecond { get; private set; } = 0;
|
||||||
private string _currentRomName = "No ROM";
|
private string _currentRomName = "No ROM";
|
||||||
private string _currentRomPath = "";
|
private string _currentRomPath = "";
|
||||||
private Stopwatch _stopwatch = new System.Diagnostics.Stopwatch();
|
private Stopwatch _stopwatch = new Stopwatch();
|
||||||
|
private string _romType = "";
|
||||||
|
private bool isTurboMode = false;
|
||||||
public bool IsRunning { get; private set; } = false;
|
public bool IsRunning { get; private set; } = false;
|
||||||
|
|
||||||
public ushort? Breakpoint
|
public ushort? Breakpoint
|
||||||
@@ -37,21 +37,17 @@ namespace Desktop
|
|||||||
public ParsonsForm1()
|
public ParsonsForm1()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
// These are perfectly safe for the Visual Studio Designer!
|
|
||||||
this.KeyPreview = true;
|
this.KeyPreview = true;
|
||||||
this.KeyDown += Form1_KeyDown;
|
this.KeyDown += Form1_KeyDown;
|
||||||
this.KeyUp += Form1_KeyUp;
|
this.KeyUp += Form1_KeyUp;
|
||||||
this.DoubleBuffered = true;
|
this.DoubleBuffered = true;
|
||||||
this.ResizeRedraw = true;
|
this.ResizeRedraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Designer ignores this completely, but the compiled game runs it instantly!
|
|
||||||
protected override void OnLoad(EventArgs e)
|
protected override void OnLoad(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnLoad(e);
|
base.OnLoad(e);
|
||||||
|
|
||||||
this.Text = $"Parsons Master System - {_currentRomName}";
|
this.Text = $"Parsons Master System 2026 - {_currentRomName}";
|
||||||
|
|
||||||
this.BackColor = Color.Black;
|
this.BackColor = Color.Black;
|
||||||
_machine = new SmsMachine();
|
_machine = new SmsMachine();
|
||||||
@@ -70,6 +66,7 @@ namespace Desktop
|
|||||||
this.Invalidate();
|
this.Invalidate();
|
||||||
TotalFrameCount++;
|
TotalFrameCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPaint(PaintEventArgs e)
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnPaint(e);
|
base.OnPaint(e);
|
||||||
@@ -80,44 +77,90 @@ namespace Desktop
|
|||||||
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
|
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
|
||||||
e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
|
e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
|
||||||
|
|
||||||
// 2. Calculate the actual usable window space
|
|
||||||
int topOffset = menuStrip1.Height;
|
int topOffset = menuStrip1.Height;
|
||||||
int availableWidth = this.ClientSize.Width;
|
int availableWidth = this.ClientSize.Width;
|
||||||
int availableHeight = this.ClientSize.Height - topOffset;
|
int availableHeight = this.ClientSize.Height - topOffset;
|
||||||
|
|
||||||
// 3. Calculate the maximum scale factor that fits perfectly
|
// 2. THE LENS LENS: Determine what part of the VDP buffer we actually want to show!
|
||||||
float scaleX = (float)availableWidth / 256f;
|
// (Safely check if the machine/video processor exists yet)
|
||||||
float scaleY = (float)availableHeight / 192f;
|
bool isGG = _machine?.VideoProcessor?.IsGameGear ?? false;
|
||||||
|
|
||||||
|
int sourceWidth = isGG ? 160 : 256;
|
||||||
|
int sourceHeight = isGG ? 144 : 192;
|
||||||
|
int sourceX = isGG ? 48 : 0; // Skip 48 pixels in!
|
||||||
|
int sourceY = isGG ? 24 : 0; // Skip 24 pixels down!
|
||||||
|
|
||||||
|
// Create a rectangle defining the specific pixels to extract from the raw frame buffer
|
||||||
|
Rectangle sourceRect = new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight);
|
||||||
|
|
||||||
|
// 3. Calculate the maximum scale factor for the cropped image
|
||||||
|
float scaleX = (float)availableWidth / sourceWidth;
|
||||||
|
float scaleY = (float)availableHeight / sourceHeight;
|
||||||
|
|
||||||
// Pick the smaller scale so the image never bleeds off the edge
|
// Pick the smaller scale so the image never bleeds off the edge
|
||||||
float scale = Math.Min(scaleX, scaleY);
|
float scale = Math.Min(scaleX, scaleY);
|
||||||
|
|
||||||
// 4. Calculate the new physical pixel dimensions
|
// 4. Calculate the new physical pixel dimensions
|
||||||
int newWidth = (int)(256 * scale);
|
int newWidth = (int)(sourceWidth * scale);
|
||||||
int newHeight = (int)(192 * scale);
|
int newHeight = (int)(sourceHeight * scale);
|
||||||
|
|
||||||
// 5. Center the image (Letterboxing / Pillarboxing)
|
// 5. Center the image (Letterboxing / Pillarboxing)
|
||||||
int offsetX = (availableWidth - newWidth) / 2;
|
int offsetX = (availableWidth - newWidth) / 2;
|
||||||
int offsetY = topOffset + ((availableHeight - newHeight) / 2);
|
int offsetY = topOffset + ((availableHeight - newHeight) / 2);
|
||||||
|
|
||||||
Rectangle renderArea = new Rectangle(offsetX, offsetY, newWidth, newHeight);
|
// Create a rectangle defining exactly where on the Windows form to draw the extracted pixels
|
||||||
|
Rectangle destRect = new Rectangle(offsetX, offsetY, newWidth, newHeight);
|
||||||
|
|
||||||
// 6. Draw the scaled image
|
// 6. Blast the perfectly cropped and scaled bitmap onto the screen!
|
||||||
e.Graphics.DrawImage(_screenBitmap, renderArea);
|
e.Graphics.DrawImage(_screenBitmap, destRect, sourceRect, GraphicsUnit.Pixel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//protected override void OnPaint(PaintEventArgs e)
|
||||||
|
//{
|
||||||
|
// base.OnPaint(e);
|
||||||
|
|
||||||
|
// if (_screenBitmap != null && menuStrip1 != null)
|
||||||
|
// {
|
||||||
|
// // 1. Maintain perfect, chunky retro pixels
|
||||||
|
// e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
|
||||||
|
// e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
|
||||||
|
|
||||||
|
// // 2. Calculate the actual usable window space
|
||||||
|
// int topOffset = menuStrip1.Height;
|
||||||
|
// int availableWidth = this.ClientSize.Width;
|
||||||
|
// int availableHeight = this.ClientSize.Height - topOffset;
|
||||||
|
|
||||||
|
// // 3. Calculate the maximum scale factor that fits perfectly
|
||||||
|
// float scaleX = (float)availableWidth / 256f;
|
||||||
|
// float scaleY = (float)availableHeight / 192f;
|
||||||
|
|
||||||
|
// // Pick the smaller scale so the image never bleeds off the edge
|
||||||
|
// float scale = Math.Min(scaleX, scaleY);
|
||||||
|
|
||||||
|
// // 4. Calculate the new physical pixel dimensions
|
||||||
|
// int newWidth = (int)(256 * scale);
|
||||||
|
// int newHeight = (int)(192 * scale);
|
||||||
|
|
||||||
|
// // 5. Center the image (Letterboxing / Pillarboxing)
|
||||||
|
// int offsetX = (availableWidth - newWidth) / 2;
|
||||||
|
// int offsetY = topOffset + ((availableHeight - newHeight) / 2);
|
||||||
|
|
||||||
|
// Rectangle renderArea = new Rectangle(offsetX, offsetY, newWidth, newHeight);
|
||||||
|
|
||||||
|
// // 6. Draw the scaled image
|
||||||
|
// e.Graphics.DrawImage(_screenBitmap, renderArea);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
public void StartEmulator()
|
public void StartEmulator()
|
||||||
{
|
{
|
||||||
if (IsRunning) return;
|
if (IsRunning) return;
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
TotalFrameCount = 0;
|
TotalFrameCount = 0;
|
||||||
|
|
||||||
double targetFps = _machine.VideoProcessor.IsPalRegion ? 50.0 : 59.92274;
|
|
||||||
double TargetFrameTime = 1000.0 / targetFps;
|
|
||||||
|
|
||||||
_emulatorTask = Task.Run(() =>
|
_emulatorTask = Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
double targetFps = 59.94;
|
||||||
|
double TargetFrameTime = 1000.0 / targetFps;
|
||||||
_stopwatch.Restart();
|
_stopwatch.Restart();
|
||||||
|
|
||||||
double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime;
|
double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime;
|
||||||
@@ -126,7 +169,13 @@ namespace Desktop
|
|||||||
|
|
||||||
while (IsRunning)
|
while (IsRunning)
|
||||||
{
|
{
|
||||||
// Mark exactly when the emulator starts thinking
|
//targetFps = isTurboMode ? 1000 : 59.94;
|
||||||
|
//TargetFrameTime = 1000.0 / targetFps;
|
||||||
|
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||||||
|
{
|
||||||
|
targetFps = isTurboMode ? 1000 : 59.94;
|
||||||
|
TargetFrameTime = 1000.0 / targetFps;
|
||||||
|
});
|
||||||
double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds;
|
double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds;
|
||||||
|
|
||||||
// --- POLL PHYSICAL CONTROLLER ---
|
// --- POLL PHYSICAL CONTROLLER ---
|
||||||
@@ -187,7 +236,7 @@ namespace Desktop
|
|||||||
|
|
||||||
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||||||
{
|
{
|
||||||
this.Text = $"Parsons Master System 2026 - {_currentRomName} [FPS/FT: {FramesPerSecond:F0}/{FrameTime:F1}]";
|
this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName} [FPS: {FramesPerSecond:F0}]";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +261,18 @@ namespace Desktop
|
|||||||
{
|
{
|
||||||
await _emulatorTask;
|
await _emulatorTask;
|
||||||
}
|
}
|
||||||
|
_romType = Path.GetExtension(filePath);
|
||||||
|
if (_romType == ".sms")
|
||||||
|
{
|
||||||
|
_machine.VideoProcessor.IsGameGear = false;
|
||||||
|
}
|
||||||
|
else if (_romType == ".gg")
|
||||||
|
{
|
||||||
|
_machine.VideoProcessor.IsGameGear = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new Exception("Incorrect ROM Type!");
|
||||||
|
_romType = _machine.VideoProcessor.IsGameGear ? "GG" : "SMS";
|
||||||
|
|
||||||
// 1. SAVE THE PREVIOUS GAME!
|
// 1. SAVE THE PREVIOUS GAME!
|
||||||
SaveCurrentSram();
|
SaveCurrentSram();
|
||||||
@@ -225,7 +286,7 @@ namespace Desktop
|
|||||||
// 4. Update the path tracking
|
// 4. Update the path tracking
|
||||||
_currentRomPath = filePath;
|
_currentRomPath = filePath;
|
||||||
_currentRomName = Path.GetFileNameWithoutExtension(filePath);
|
_currentRomName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
this.Text = $"Parsons Master System - {_currentRomName}";
|
//this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}";
|
||||||
|
|
||||||
// 5. LOAD THE NEW SAVE DATA FROM THE EXE FOLDER!
|
// 5. LOAD THE NEW SAVE DATA FROM THE EXE FOLDER!
|
||||||
string savPath = GetSaveFilePath();
|
string savPath = GetSaveFilePath();
|
||||||
@@ -263,13 +324,11 @@ namespace Desktop
|
|||||||
|
|
||||||
private void PopulateIncludedRomsMenu()
|
private void PopulateIncludedRomsMenu()
|
||||||
{
|
{
|
||||||
// The folder you used for Golden Axe Warrior
|
|
||||||
string romsDirectory = @"C:\Parsons\Local Code Projects\ParsonsMasterSystem2026\Desktop\ROMS\";
|
string romsDirectory = @"C:\Parsons\Local Code Projects\ParsonsMasterSystem2026\Desktop\ROMS\";
|
||||||
|
|
||||||
if (Directory.Exists(romsDirectory))
|
if (Directory.Exists(romsDirectory))
|
||||||
{
|
{
|
||||||
string[] romFiles = Directory.GetFiles(romsDirectory, "*.sms");
|
string[] romFiles = Directory.GetFiles(romsDirectory, "*.sms");
|
||||||
|
|
||||||
foreach (string file in romFiles)
|
foreach (string file in romFiles)
|
||||||
{
|
{
|
||||||
// Create a new dropdown item for each .sms file it finds
|
// Create a new dropdown item for each .sms file it finds
|
||||||
@@ -280,8 +339,20 @@ namespace Desktop
|
|||||||
romMenuItem.Click += (sender, e) => LoadRomAndStart(file);
|
romMenuItem.Click += (sender, e) => LoadRomAndStart(file);
|
||||||
|
|
||||||
// Add it to the "Included" submenu (make sure the name matches your designer element!)
|
// Add it to the "Included" submenu (make sure the name matches your designer element!)
|
||||||
includedToolStripMenuItem.DropDownItems.Add(romMenuItem);
|
masterSystemToolStripMenuItem.DropDownItems.Add(romMenuItem);
|
||||||
}
|
}
|
||||||
|
romFiles = Directory.GetFiles(romsDirectory, "*.gg");
|
||||||
|
foreach (string file in romFiles)
|
||||||
|
{
|
||||||
|
// Create a new dropdown item for each .gg file it finds
|
||||||
|
string romName = Path.GetFileNameWithoutExtension(file);
|
||||||
|
ToolStripMenuItem romMenuItem = new ToolStripMenuItem(romName);
|
||||||
|
|
||||||
|
// When clicked, pass the file path to the helper method
|
||||||
|
romMenuItem.Click += (sender, e) => LoadRomAndStart(file);
|
||||||
|
gameGearToolStripMenuItem.DropDownItems.Add(romMenuItem);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +361,7 @@ namespace Desktop
|
|||||||
using (OpenFileDialog ofd = new OpenFileDialog())
|
using (OpenFileDialog ofd = new OpenFileDialog())
|
||||||
{
|
{
|
||||||
ofd.Title = "Select Master System ROM";
|
ofd.Title = "Select Master System ROM";
|
||||||
ofd.Filter = "SMS ROMs (*.sms)|*.sms|All Files (*.*)|*.*";
|
ofd.Filter = "SMS ROMs (*.sms)|*.sms|GG ROMs (*.gg)|*.gg|All Files (*.*)|*.*";
|
||||||
|
|
||||||
if (ofd.ShowDialog() == DialogResult.OK)
|
if (ofd.ShowDialog() == DialogResult.OK)
|
||||||
{
|
{
|
||||||
@@ -324,10 +395,6 @@ namespace Desktop
|
|||||||
_vramViewer.Show();
|
_vramViewer.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void includedToolStripMenuItem_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
|
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
@@ -412,14 +479,10 @@ namespace Desktop
|
|||||||
StartEmulator();
|
StartEmulator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pALRegionToolStripMenuItem_Click(object sender, EventArgs e)
|
private void turboModeToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (pALRegionToolStripMenuItem.Checked)
|
turboModeToolStripMenuItem.Checked = !turboModeToolStripMenuItem.Checked;
|
||||||
_machine.VideoProcessor.IsPalRegion = true;
|
isTurboMode = turboModeToolStripMenuItem.Checked ? true : false;
|
||||||
else _machine.VideoProcessor.IsPalRegion= false;
|
|
||||||
|
|
||||||
pALRegionToolStripMenuItem.Checked = !pALRegionToolStripMenuItem.Checked;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Desktop
|
|||||||
this.Text = "VRAM Viewer (512 Tiles)";
|
this.Text = "VRAM Viewer (512 Tiles)";
|
||||||
|
|
||||||
// 256x128 native resolution, scaled up by 2 for visibility!
|
// 256x128 native resolution, scaled up by 2 for visibility!
|
||||||
this.ClientSize = new Size(1024, 512);
|
this.ClientSize = new Size(512, 256);
|
||||||
this.DoubleBuffered = true;
|
this.DoubleBuffered = true;
|
||||||
this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
|
this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
|
||||||
|
|
||||||
@@ -45,7 +45,6 @@ namespace Desktop
|
|||||||
int tileX = (tile % 32) * 8;
|
int tileX = (tile % 32) * 8;
|
||||||
int tileY = (tile / 32) * 8;
|
int tileY = (tile / 32) * 8;
|
||||||
int tileAddress = tile * 32;
|
int tileAddress = tile * 32;
|
||||||
|
|
||||||
// Decode the 8x8 pixels
|
// Decode the 8x8 pixels
|
||||||
for (int y = 0; y < 8; y++)
|
for (int y = 0; y < 8; y++)
|
||||||
{
|
{
|
||||||
@@ -57,19 +56,19 @@ namespace Desktop
|
|||||||
for (int x = 0; x < 8; x++)
|
for (int x = 0; x < 8; x++)
|
||||||
{
|
{
|
||||||
int shift = 7 - x;
|
int shift = 7 - x;
|
||||||
int colorIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) |
|
int colourIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) |
|
||||||
(((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3);
|
(((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3);
|
||||||
|
|
||||||
// Use the Sprite Palette (Offset 16) so characters and enemies are colored correctly
|
// Use the Sprite Palette (Offset 16) so characters and enemies are colored correctly
|
||||||
byte smsColor = _vdp.CRAM[16 + colorIndex];
|
int colour = _vdp.GetRgbColour(16, colourIndex);
|
||||||
int r = (smsColor & 0x03) * 85;
|
//int r = (smsColor & 0x03) * 85;
|
||||||
int g = ((smsColor >> 2) & 0x03) * 85;
|
//int g = ((smsColor >> 2) & 0x03) * 85;
|
||||||
int b = ((smsColor >> 4) & 0x03) * 85;
|
//int b = ((smsColor >> 4) & 0x03) * 85;
|
||||||
|
|
||||||
// Background color (Index 0) is technically transparent for sprites, so we render it as magenta
|
// Background color (Index 0) is technically transparent for sprites, so we render it as magenta
|
||||||
if (colorIndex == 0) { r = 255; g = 0; b = 255; }
|
//if (colourIndex == 0) { int r = 255; int g = 0; int b = 255; }
|
||||||
|
|
||||||
_pixelData[((tileY + y) * 256) + (tileX + x)] = (255 << 24) | (r << 16) | (g << 8) | b;
|
_pixelData[((tileY + y) * 256) + (tileX + x)] = colourIndex == 0 ? (255 << 24) | (255 << 16) | (0 << 8) | 255 : colour;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user