#include "PlasmaDisplay.h" // Command bytes (see PlasmaProtocol.cs / FIRMWARE.md). static const uint8_t ESC = 0x1B; static const uint8_t BS = 0x08, HT = 0x09, LF = 0x0A, VT = 0x0B, CR = 0x0D; static const uint8_t CMD_CLEAR = '@', CMD_CURSOR = 'G', CMD_ATTR = 'H'; static const uint8_t CMD_FONT = 'K', CMD_HOME = 'L', CMD_GRAPHICS = 'P'; static const uint8_t CMD_SETROW = 'Q', CMD_SETCOL = 'R'; static const uint8_t CMD_DRAWPAGE = 'I', CMD_DISPPAGE = 'i'; static const uint8_t CMD_BOXDRAW = 'X', CMD_BOXFILL = 'x', CMD_SCREENOP = 'Y'; PlasmaDisplay::PlasmaDisplay() { reset(); } void PlasmaDisplay::reset() { memset(pixels_, 0, sizeof(pixels_)); cx_ = cy_ = 0; font_ = 0; face_ = &plasmaFonts[0]; attrs_ = 0; cursorMode_ = CURSOR_STEADY; // power-on default; the game hides it orient_ = HORIZONTAL; state_ = TEXT; headerFill_ = 0; dataIndex_ = dataLength_ = 0; boxFill_ = 0; dirty_ = true; } void PlasmaDisplay::setOrientation(Orientation o) { if (orient_ == o) return; orient_ = o; memset(pixels_, 0, sizeof(pixels_)); cx_ = cy_ = 0; dirty_ = true; } // The real display's power-on test (JP1 jumper 5, firmware $B888) runs through // several drawing sequences — solid, border+grid, and more — to expose dead // dots and addressing faults. These stand in for that; the sketch cycles them. static bool testDot(int index, int x, int y) { switch (index) { case 0: return true; // all on case 1: return x == 0 || x == PlasmaDisplay::WIDTH - 1 || // frame y == 0 || y == PlasmaDisplay::HEIGHT - 1 || (x % 16) == 0 || (y % 8) == 0; // + grid case 2: return ((y / 2) % 2) == 0; // horizontal stripes case 3: return ((x / 4) % 2) == 0; // vertical stripes default: return (((x / 8) + (y / 8)) % 2) == 0; // checkerboard } } void PlasmaDisplay::showTestPattern(int index) { for (int y = 0; y < HEIGHT; ++y) for (int x = 0; x < WIDTH; ++x) pixels_[y * WIDTH + x] = testDot(index, x, y) ? PIX_LIT : 0; dirty_ = true; } // One logical dot → the physical 128x32 buffer, rotated per orientation. void PlasmaDisplay::plot(int lx, int ly, uint8_t flags) { if ((unsigned)lx >= (unsigned)logicalW() || (unsigned)ly >= (unsigned)logicalH()) return; int px, py; if (orient_ == HORIZONTAL) { px = lx; py = ly; } else { px = ly; py = HEIGHT - 1 - lx; // 90° rotation onto landscape glass } pixels_[py * WIDTH + px] = flags; } uint8_t PlasmaDisplay::peek(int lx, int ly) const { if ((unsigned)lx >= (unsigned)logicalW() || (unsigned)ly >= (unsigned)logicalH()) return 0; int px, py; if (orient_ == HORIZONTAL) { px = lx; py = ly; } else { px = ly; py = HEIGHT - 1 - lx; } return pixels_[py * WIDTH + px]; } void PlasmaDisplay::feed(const uint8_t *buf, size_t n) { for (size_t i = 0; i < n; ++i) feed(buf[i]); } void PlasmaDisplay::feed(uint8_t b) { switch (state_) { case TEXT: stepText(b); break; case ESCAPE: stepEscape(b); break; case OPERAND: state_ = TEXT; applyOperand(pendingCmd_, b); break; case GHEADER: header_[headerFill_++] = b; if (headerFill_ == 5) beginGraphics(); break; case GDATA: stepGraphics(b); break; case BOXOP: stepBoxOperand(b); break; } } void PlasmaDisplay::stepText(uint8_t b) { switch (b) { case ESC: state_ = ESCAPE; return; case BS: cx_ = max(0, cx_ - face_->width); dirty_ = true; return; case HT: advanceCursor(); dirty_ = true; return; case LF: nextLine(); dirty_ = true; return; case VT: cy_ -= face_->height; if (cy_ < 0) cy_ = max(0, logicalH() - face_->height); dirty_ = true; return; case CR: cx_ = 0; dirty_ = true; return; } if (b < 0x20) return; // any other control byte: swallow (matches firmware) drawChar(b); } void PlasmaDisplay::stepEscape(uint8_t b) { state_ = TEXT; switch (b) { case CMD_CLEAR: memset(pixels_, 0, sizeof(pixels_)); cx_ = cy_ = 0; font_ = 0; face_ = &plasmaFonts[0]; attrs_ = 0; dirty_ = true; break; case CMD_HOME: cx_ = cy_ = 0; dirty_ = true; break; case CMD_CURSOR: case CMD_FONT: case CMD_ATTR: case CMD_SETROW: case CMD_SETCOL: case CMD_DRAWPAGE: case CMD_DISPPAGE: pendingCmd_ = b; state_ = OPERAND; break; case CMD_GRAPHICS: headerFill_ = 0; state_ = GHEADER; break; case CMD_BOXDRAW: case CMD_BOXFILL: case CMD_SCREENOP: boxCmd_ = b; boxFill_ = 0; state_ = BOXOP; break; default: break; // unknown command: ignored (does not consume an operand) } } void PlasmaDisplay::applyOperand(uint8_t cmd, uint8_t op) { switch (cmd) { case CMD_CURSOR: cursorMode_ = (op == 0x00 || op == 0xFF) ? CURSOR_HIDDEN : (op & 0x02) ? CURSOR_FLASHING : CURSOR_STEADY; dirty_ = true; break; case CMD_FONT: if (op < 8) { // 8 real fonts; firmware ignores larger operands font_ = op; face_ = &plasmaFonts[op]; if (cx_ > logicalW() - 1) cx_ = logicalW() - 1; if (cy_ > logicalH() - 1) cy_ = logicalH() - 1; dirty_ = true; } break; case CMD_ATTR: attrs_ = op & 0x0F; // low 4 bits: half/underline/reverse/flash break; case CMD_SETROW: if (op < logicalH()) { cy_ = op; dirty_ = true; } break; case CMD_SETCOL: if (op < logicalW()) { cx_ = op; dirty_ = true; } break; case CMD_DRAWPAGE: case CMD_DISPPAGE: break; // page select: consumed but single-page (see FIRMWARE.md) } } // ESC X / ESC x / ESC Y collect operands one byte at a time, each validated on // arrival; an out-of-range operand aborts the whole sequence with nothing drawn // (firmware $A748/$A91A/$A644: BCLR $AF #$10). Operand bytes are never // re-tokenized, so coordinate values that collide with control bytes are fine — // Firestorm's real panel left edge is 27, which is ESC on the wire. void PlasmaDisplay::stepBoxOperand(uint8_t b) { bool ok; if (boxCmd_ == CMD_SCREENOP) { // ESC Y mode,srcA,srcB,dest — whole-screen AND/OR/XOR between page // buffers. Validated and consumed but a no-op here: single-page, like // ESC I / ESC i. ok = (boxFill_ == 0) ? (b <= 2) : (b <= 9); if (!ok) { state_ = TEXT; return; } box_[boxFill_++] = b; if (boxFill_ == 4) state_ = TEXT; return; } // ESC x leads with a mode byte; the coordinates then follow in both commands // as left, top, right, bottom — inclusive, right >= left, bottom >= top. int coordIdx = (boxCmd_ == CMD_BOXFILL) ? boxFill_ - 1 : boxFill_; switch (coordIdx) { case -1: ok = b <= 2; break; // mode: 0 blank, 1 fill, 2 invert case 0: ok = b < logicalW(); break; case 1: ok = b < logicalH(); break; case 2: ok = b < logicalW() && b >= box_[boxFill_ - 2]; break; default: ok = b < logicalH() && b >= box_[boxFill_ - 2]; break; } if (!ok) { state_ = TEXT; return; } box_[boxFill_++] = b; int need = (boxCmd_ == CMD_BOXFILL) ? 5 : 4; if (boxFill_ < need) return; state_ = TEXT; if (boxCmd_ == CMD_BOXDRAW) drawBoxOutline(box_[0], box_[1], box_[2], box_[3]); else applyBoxRegion(box_[0], box_[1], box_[2], box_[3], box_[4]); } // ESC X: rectangle outline ($A748) — solid top row; if taller than one row, // solid bottom row with single-dot side columns between. Dots are OR-set, so // existing content survives (and a half-intensity dot goes full, as the // firmware sets the bit in both page halves). void PlasmaDisplay::drawBoxOutline(int l, int t, int r, int b) { for (int x = l; x <= r; ++x) plot(x, t, PIX_LIT); if (b > t) { for (int x = l; x <= r; ++x) plot(x, b, PIX_LIT); for (int y = t + 1; y < b; ++y) { plot(l, y, PIX_LIT); plot(r, y, PIX_LIT); } } dirty_ = true; } // ESC x: rectangular region op ($A91A, fill worker $AA07) — mode 0 blanks, // 1 fills solid, 2 inverts. Firestorm always sends mode 0 to wipe the score // panel before boxing it. void PlasmaDisplay::applyBoxRegion(int mode, int l, int t, int r, int b) { for (int y = t; y <= b; ++y) for (int x = l; x <= r; ++x) { switch (mode) { case 0: plot(x, y, 0); break; case 1: plot(x, y, PIX_LIT); break; default: plot(x, y, (peek(x, y) & PIX_LIT) ? 0 : PIX_LIT); break; } } dirty_ = true; } void PlasmaDisplay::beginGraphics() { int w = header_[3], h = header_[4]; dataLength_ = w * h; dataIndex_ = 0; state_ = dataLength_ > 0 ? GDATA : TEXT; } void PlasmaDisplay::stepGraphics(uint8_t b) { int w = header_[3]; int rowOfBlock = dataIndex_ / w; int byteOfRow = dataIndex_ % w; int y = header_[1] + rowOfBlock; int baseX = (header_[2] + byteOfRow) * 8; // MSB = leftmost pixel (L4PLASMA.CPP packs 0x80 first). for (int bit = 0; bit < 8; ++bit) plot(baseX + bit, y, (b & (0x80 >> bit)) ? PIX_LIT : 0); dirty_ = true; if (++dataIndex_ >= dataLength_) state_ = TEXT; } void PlasmaDisplay::drawChar(uint8_t code) { if (!faceHas(code)) return; // firmware ignores out-of-range chars int w = face_->width, h = face_->height; bool reverse = attrs_ & 0x04; bool underline = attrs_ & 0x02; uint8_t litFlags = PIX_LIT; if (attrs_ & 0x01) litFlags |= PIX_HALF; if (attrs_ & 0x08) litFlags |= PIX_FLASH; for (int row = 0; row < h; ++row) { uint16_t bits = faceRow(code, row); // bit15 = leftmost pixel for (int col = 0; col < w; ++col) { bool on = bits & (0x8000 >> col); if (underline && row == h - 1) on = true; if (reverse) on = !on; plot(cx_ + col, cy_ + row, on ? litFlags : 0); } } dirty_ = true; advanceCursor(); } void PlasmaDisplay::advanceCursor() { cx_ += face_->width; if (cx_ > logicalW() - face_->width) nextLine(); } void PlasmaDisplay::nextLine() { cx_ = 0; cy_ += face_->height; if (cy_ > logicalH() - face_->height) cy_ = 0; // wrap to top; no scroll }