Files
VRIO/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs
T
CydandClaude Opus 4.8 79eb53253e vPLASMA (standalone): vRIO-style picker, real firmware demo, PD5 fix
- Connection: drop the hardwired COM12; replicate vRIO's endpoint picker —
  a combo of pipe:vplasma (the DOSBox-X namedpipe backend) + the COM ports,
  with Rescan and Open/Close. Nothing opens automatically; baud straps drive
  a COM open.
- Demo (jumper 6) now runs the REAL firmware demonstration, not the vPLASMA
  self-test. The 10 demo screens are extracted verbatim from the ROM demo
  pointer table ($8000) into PlasmaFirmwareDemo.cs and looped through the
  parser. Added ESC I / ESC i (draw/display page select) as 1-operand
  commands — consumed but not acted on in the single-page model — so the
  demo's page commands don't desync the stream.
- Orientation (jumper 4 / PD5) polarity fixed: unstrapped = horizontal
  128x32 (the normal cockpit setup, now the default), installed = vertical.

Verified: 29 unit tests pass (2 new: demo replay, page-select operand); the
real demo screens render with the correct text/positioning/fonts, and the
picker lists pipe:vplasma + COM ports with no auto-open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 19:42:55 -05:00

399 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using VPlasma.Core.Device;
using Xunit;
namespace VPlasma.Core.Tests;
public class VPlasmaDeviceTests
{
private const byte Esc = 0x1B;
private static void Feed(VPlasmaDevice device, params byte[] bytes)
=> device.OnReceived(bytes, bytes.Length);
private static void Feed(VPlasmaDevice device, IEnumerable<byte> bytes)
{
byte[] arr = bytes.ToArray();
device.OnReceived(arr, arr.Length);
}
private static byte Pixel(VPlasmaDevice device, int x, int y)
{
var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height];
device.CopyFrame(frame);
return frame[y * VPlasmaDevice.Width + x];
}
/// <summary>A full-width ESC P row the way L4PLASMA.CPP sends one.</summary>
private static byte[] GraphicsRow(int y, params byte[] data)
{
var row = new List<byte> { Esc, (byte)'P', 0, (byte)y, 0, (byte)data.Length, 1 };
row.AddRange(data);
return row.ToArray();
}
// ---- graphics writes -------------------------------------------------
[Fact]
public void GraphicsRow_SetsPixelsMsbFirst()
{
var device = new VPlasmaDevice();
Feed(device, GraphicsRow(5, 0x80, 0x01)); // xbyte 0..1
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 5)); // MSB of byte 0
Assert.Equal(0, Pixel(device, 1, 5));
Assert.Equal(0, Pixel(device, 14, 5));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 15, 5)); // LSB of byte 1
Assert.Equal(0, Pixel(device, 0, 4));
Assert.Equal(0, Pixel(device, 0, 6));
Assert.Equal(1, device.GraphicsRows);
}
[Fact]
public void GraphicsRow_SurvivesAnyChunkBoundary()
{
byte[] wire = GraphicsRow(3, Enumerable.Repeat((byte)0xFF, 16).ToArray());
for (int split = 1; split < wire.Length; ++split)
{
var device = new VPlasmaDevice();
device.OnReceived(wire, split);
byte[] rest = wire.Skip(split).ToArray();
device.OnReceived(rest, rest.Length);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 3));
Assert.Equal(1, device.GraphicsRows);
}
}
[Fact]
public void GraphicsBlock_MultipleRowsAdvanceY()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'P', 0, 10, 0, 1, 3, 0x80, 0x80, 0x80);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 10));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 11));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 12));
Assert.Equal(0, Pixel(device, 0, 13));
Assert.Equal(3, device.GraphicsRows);
}
[Fact]
public void GraphicsWrite_HonorsByteColumnOffset()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'P', 0, 0, 2, 1, 1, 0xFF); // xbyte=2 → x 16..23
Assert.Equal(0, Pixel(device, 15, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 16, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 23, 0));
Assert.Equal(0, Pixel(device, 24, 0));
}
[Fact]
public void GraphicsWrite_OutOfRangeRowIsConsumedNotDrawn()
{
var device = new VPlasmaDevice();
Feed(device, GraphicsRow(40, Enumerable.Repeat((byte)0xFF, 16).ToArray()));
Feed(device, (byte)'H'); // parser must be back in text mode
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(0, device.GraphicsRows);
}
[Fact]
public void GraphicsWrite_OverwritesTextAttributes()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'H', 8); // flashing text
Feed(device, (byte)'H'); // font-0 'H' lights (0,0)
Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelFlash, Pixel(device, 0, 0));
Feed(device, GraphicsRow(0, 0x80)); // repaint that row from the wire
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
}
// ---- text mode (real font 0 = 6×8) -----------------------------------
[Fact]
public void Text_DrawsRealGlyphAndAdvancesByFontWidth()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H'); // font-0 'H': row0 = pixels at x0 and x4
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 4, 0));
Assert.Equal(0, Pixel(device, 1, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3)); // crossbar row
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 4, 3));
Assert.Equal(0, Pixel(device, 0, 7)); // blank bottom row
Assert.Equal(6, device.CursorX); // advanced one 6-px cell
Assert.Equal(0, device.CursorY);
Assert.Equal(1, device.TextCharsDrawn);
}
[Fact]
public void ControlChars_MoveCursorByPixels()
{
var device = new VPlasmaDevice(); // font 0: 6 wide, 8 tall
Feed(device, 0x09, 0x09, 0x09); // HT ×3 → x = 18
Assert.Equal(18, device.CursorX);
Feed(device, 0x08); // BS → x = 12
Assert.Equal(12, device.CursorX);
Feed(device, 0x0A); // LF → y = 8
Assert.Equal(8, device.CursorY);
Assert.Equal(0, device.CursorX);
Feed(device, (byte)'H', 0x0D); // draw + CR → x back to 0
Assert.Equal(0, device.CursorX);
Feed(device, 0x0B); // VT → y back to 0
Assert.Equal(0, device.CursorY);
}
[Fact]
public void EscQ_EscR_SetPixelCursor()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'Q', 20); // set row Y=20
Feed(device, Esc, (byte)'R', 37); // set col X=37
Assert.Equal(37, device.CursorX);
Assert.Equal(20, device.CursorY);
Feed(device, Esc, (byte)'Q', 40); // out of range (>31): ignored
Assert.Equal(20, device.CursorY);
Feed(device, Esc, (byte)'R', 200); // out of range (>127): ignored
Assert.Equal(37, device.CursorX);
}
[Fact]
public void Text_WrapsAtRightAndBottom()
{
var device = new VPlasmaDevice(); // 21 cells of 6px per line, 4 lines
Feed(device, Enumerable.Repeat((byte)'X', 21)); // fills first line, wraps
Assert.Equal(0, device.CursorX);
Assert.Equal(8, device.CursorY);
Feed(device, Enumerable.Repeat((byte)'X', 21 * 3)); // to the end, wraps to top
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
}
[Fact]
public void EscAt_ClearsScreenAndResetsTextState()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 4, Esc, (byte)'H', 1, (byte)'H');
Feed(device, Esc, (byte)'@');
Assert.Equal(0, Pixel(device, 0, 0));
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
Assert.Equal(0, device.Font);
Assert.Equal(PlasmaAttributes.None, device.Attributes);
}
[Fact]
public void EscL_HomesCursorWithoutClearing()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H', Esc, (byte)'L');
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); // glyph survives
}
[Theory]
[InlineData(0x00, PlasmaCursorMode.Hidden)] // the game's cursor-off
[InlineData(0xFF, PlasmaCursorMode.Hidden)] // the test tool's hide
[InlineData(0x01, PlasmaCursorMode.Steady)]
[InlineData(0x03, PlasmaCursorMode.Flashing)]
public void EscG_SetsCursorMode(byte operand, PlasmaCursorMode expected)
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'G', operand);
Assert.Equal(expected, device.CursorMode);
}
[Fact]
public void EscK_SelectsRealFonts()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 4); // large font: 12×16
Assert.Equal(4, device.Font);
Assert.Equal(12, device.FontWidth);
Assert.Equal(16, device.FontHeight);
Feed(device, Esc, (byte)'K', 0); // back to font 0: 6×8
Assert.Equal(0, device.Font);
Assert.Equal(6, device.FontWidth);
Assert.Equal(8, device.FontHeight);
Feed(device, Esc, (byte)'K', 0xFF); // out of range: firmware ignores it
Assert.Equal(0, device.Font);
}
[Fact]
public void EscH_AppliesAttributesAsLowFourBits()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'H', 1, (byte)'H'); // half intensity
Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelHalf, Pixel(device, 0, 0));
Feed(device, Esc, (byte)'L', Esc, (byte)'@'); // clear
Feed(device, Esc, (byte)'H', 2, (byte)'H'); // underline: bottom row (y=7) lit
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 7));
Feed(device, Esc, (byte)'@');
Feed(device, Esc, (byte)'H', 4, (byte)' '); // reverse: a space renders solid
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 5, 7));
Feed(device, Esc, (byte)'H', 0); // defaults restored
Assert.Equal(PlasmaAttributes.None, device.Attributes);
}
// ---- robustness ----------------------------------------------------------
[Fact]
public void UnknownEscape_IsConsumedAndTextResumes()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'\\', (byte)'H'); // ESC '\' is not a command
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
}
[Fact]
public void GameStartupSequence_HidesCursor()
{
var device = new VPlasmaDevice();
Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); // power-on default
Feed(device, 27, (byte)'G', 0x00); // L4PLASMA.CPP's boot byte
Assert.Equal(PlasmaCursorMode.Hidden, device.CursorMode);
}
[Fact]
public void SelfTestPages_ParseWithoutUnknownCommands()
{
for (int page = 0; page < PlasmaSelfTest.PageCount; ++page)
{
var device = new VPlasmaDevice();
var complaints = new List<string>();
device.Logged += line =>
{
if (line.StartsWith("Unknown", StringComparison.Ordinal)
|| line.StartsWith("Unhandled", StringComparison.Ordinal))
complaints.Add(line);
};
byte[] bytes = PlasmaSelfTest.BuildPage(page);
device.OnReceived(bytes, bytes.Length);
Assert.Empty(complaints);
Assert.True(device.BytesReceived > 0);
}
}
// ---- orientation + test pattern (JP1 jumpers 4 & 5) --------------------
[Fact]
public void Orientation_VerticalRotatesLogicalOntoPhysicalGlass()
{
var device = new VPlasmaDevice { Orientation = PlasmaOrientation.Vertical };
Assert.Equal(32, device.LogicalWidth);
Assert.Equal(128, device.LogicalHeight);
// Logical (0,0) maps to physical (px=ly=0, py=Height-1-lx=31).
Feed(device, Esc, (byte)'R', 0, Esc, (byte)'Q', 0, (byte)'H');
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 31)); // 'H' top-left dot
Assert.Equal(0, Pixel(device, 0, 0));
// ESC Q (row Y) now accepts up to the 128-tall logical height;
// ESC R (column X) is bounded by the 32-wide logical width.
Feed(device, Esc, (byte)'Q', 100);
Assert.Equal(100, device.CursorY);
Feed(device, Esc, (byte)'R', 40); // out of range for 32-wide vertical
Assert.NotEqual(40, device.CursorX);
}
[Fact]
public void Orientation_ChangeClearsAndHomes()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H');
Assert.NotEqual(0, Pixel(device, 0, 0));
device.Orientation = PlasmaOrientation.Vertical;
Assert.Equal(0, Pixel(device, 0, 0)); // cleared
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
}
[Fact]
public void ShowTestPattern_LightsEveryDot()
{
var device = new VPlasmaDevice();
device.ShowTestPattern();
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 31));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 64, 16));
device.Reset();
Assert.Equal(0, Pixel(device, 64, 16));
}
[Fact]
public void FirmwareDemo_ReplaysAllScreensAndDrawsText()
{
Assert.Equal(10, PlasmaFirmwareDemo.Count);
var device = new VPlasmaDevice();
foreach (byte[] screen in PlasmaFirmwareDemo.Screens)
{
Assert.NotEmpty(screen);
device.OnReceived(screen, screen.Length);
}
// The 10-screen demo puts plenty of real text on the glass.
Assert.True(device.TextCharsDrawn > 0);
}
[Fact]
public void PageSelect_ConsumesItsOperand()
{
var device = new VPlasmaDevice();
// ESC I 2 (draw page) must swallow the '2'; then 'H' is the first glyph.
Feed(device, Esc, (byte)'I', 2, Esc, (byte)'i', 3, (byte)'H');
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
Assert.Equal(6, device.CursorX); // advanced exactly one cell (no stray chars)
}
[Fact]
public void Reset_RestoresPowerOnState()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 5, Esc, (byte)'H', 4, Esc, (byte)'G', 0, (byte)'H');
device.Reset();
Assert.Equal(0, Pixel(device, 0, 0));
Assert.Equal(0, device.Font);
Assert.Equal(PlasmaAttributes.None, device.Attributes);
Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode);
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
}
}