// AGI v2 text plane: status line, display() rows, print() modal. // // Renders directly to the stage with an embedded 8x8 ASCII bitmap // font; bypasses jlDrawText so glyph foreground/background can vary // per-call without rebuilding a font surface for every color combo. // // Layout matches AGI's 40x25 text grid: // Row 0 : status line (when statusLineOn) // Rows 1..21 : picture (handled by agiPicBlit, not here) // Rows 22..24 : message overlay area // // The print() modal centers a window over the picture region with // a 1-character border and waits for user ack (HALT_PRINT_PENDING). #include "agi.h" #include "joey/draw.h" #include "joey/surface.h" #include "surfaceInternal.h" #include #include #ifdef JOEYLIB_PLATFORM_IIGS segment "AGITXT"; #endif #define GLYPH_W 8u #define GLYPH_H 8u #define FIRST_CHAR 0x20u // space #define LAST_CHAR 0x7Eu // tilde #define GLYPH_COUNT (LAST_CHAR - FIRST_CHAR + 1u) #define STAGE_W SURFACE_WIDTH #define STAGE_H SURFACE_HEIGHT // ----- Prototypes ----- static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg); static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg); static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg); static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border); static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg); // 8x8 bitmap font for printable ASCII (0x20 = space .. 0x7E = tilde). // Each glyph is 8 bytes; bit 7 is the leftmost pixel of each row. // Lower-case letters map to upper-case via the ASCII normalizer in // drawChar so the table only needs the upper half. // // Hand-pixeled to be readable rather than authentic. Each row is // commented with the visual pattern (X = on, . = off). static const uint8_t kFont[GLYPH_COUNT][GLYPH_H] = { /* 0x20 ' ' */ {0,0,0,0,0,0,0,0}, /* 0x21 '!' */ {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00}, /* 0x22 '"' */ {0x6C,0x6C,0x48,0x00,0x00,0x00,0x00,0x00}, /* 0x23 '#' */ {0x36,0x36,0x7F,0x36,0x7F,0x36,0x36,0x00}, /* 0x24 '$' */ {0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00}, /* 0x25 '%' */ {0x66,0x66,0x0C,0x18,0x30,0x66,0x66,0x00}, /* 0x26 '&' */ {0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00}, /* 0x27 '\''*/ {0x18,0x18,0x10,0x00,0x00,0x00,0x00,0x00}, /* 0x28 '(' */ {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00}, /* 0x29 ')' */ {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00}, /* 0x2A '*' */ {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00}, /* 0x2B '+' */ {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00}, /* 0x2C ',' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30}, /* 0x2D '-' */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00}, /* 0x2E '.' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00}, /* 0x2F '/' */ {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00}, /* 0x30 '0' */ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, /* 0x31 '1' */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, /* 0x32 '2' */ {0x3C,0x66,0x06,0x1C,0x30,0x66,0x7E,0x00}, /* 0x33 '3' */ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, /* 0x34 '4' */ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, /* 0x35 '5' */ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, /* 0x36 '6' */ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, /* 0x37 '7' */ {0x7E,0x66,0x0C,0x18,0x18,0x18,0x18,0x00}, /* 0x38 '8' */ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, /* 0x39 '9' */ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, /* 0x3A ':' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00}, /* 0x3B ';' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30}, /* 0x3C '<' */ {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00}, /* 0x3D '=' */ {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00}, /* 0x3E '>' */ {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00}, /* 0x3F '?' */ {0x3C,0x66,0x06,0x0C,0x18,0x00,0x18,0x00}, /* 0x40 '@' */ {0x3C,0x66,0x6E,0x6E,0x60,0x62,0x3C,0x00}, /* 0x41 'A' */ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, /* 0x42 'B' */ {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00}, /* 0x43 'C' */ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, /* 0x44 'D' */ {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00}, /* 0x45 'E' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x7E,0x00}, /* 0x46 'F' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x60,0x00}, /* 0x47 'G' */ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00}, /* 0x48 'H' */ {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, /* 0x49 'I' */ {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, /* 0x4A 'J' */ {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00}, /* 0x4B 'K' */ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, /* 0x4C 'L' */ {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00}, /* 0x4D 'M' */ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, /* 0x4E 'N' */ {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00}, /* 0x4F 'O' */ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, /* 0x50 'P' */ {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00}, /* 0x51 'Q' */ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, /* 0x52 'R' */ {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00}, /* 0x53 'S' */ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, /* 0x54 'T' */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, /* 0x55 'U' */ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, /* 0x56 'V' */ {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00}, /* 0x57 'W' */ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, /* 0x58 'X' */ {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00}, /* 0x59 'Y' */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, /* 0x5A 'Z' */ {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00}, /* 0x5B '[' */ {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00}, /* 0x5C '\\'*/ {0xC0,0x60,0x30,0x18,0x0C,0x06,0x02,0x00}, /* 0x5D ']' */ {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00}, /* 0x5E '^' */ {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00}, /* 0x5F '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF}, /* 0x60 '`' */ {0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00}, /* 0x61 'a' */ {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00}, /* 0x62 'b' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00}, /* 0x63 'c' */ {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00}, /* 0x64 'd' */ {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00}, /* 0x65 'e' */ {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00}, /* 0x66 'f' */ {0x1C,0x36,0x30,0x78,0x30,0x30,0x30,0x00}, /* 0x67 'g' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C}, /* 0x68 'h' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00}, /* 0x69 'i' */ {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00}, /* 0x6A 'j' */ {0x06,0x00,0x06,0x06,0x06,0x06,0x66,0x3C}, /* 0x6B 'k' */ {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00}, /* 0x6C 'l' */ {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, /* 0x6D 'm' */ {0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00}, /* 0x6E 'n' */ {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00}, /* 0x6F 'o' */ {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00}, /* 0x70 'p' */ {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60}, /* 0x71 'q' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06}, /* 0x72 'r' */ {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00}, /* 0x73 's' */ {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00}, /* 0x74 't' */ {0x18,0x18,0x7E,0x18,0x18,0x18,0x0E,0x00}, /* 0x75 'u' */ {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00}, /* 0x76 'v' */ {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00}, /* 0x77 'w' */ {0x00,0x00,0x63,0x6B,0x7F,0x7F,0x36,0x00}, /* 0x78 'x' */ {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00}, /* 0x79 'y' */ {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C}, /* 0x7A 'z' */ {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00}, /* 0x7B '{' */ {0x0E,0x18,0x18,0x70,0x18,0x18,0x0E,0x00}, /* 0x7C '|' */ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, /* 0x7D '}' */ {0x70,0x18,0x18,0x0E,0x18,0x18,0x70,0x00}, /* 0x7E '~' */ {0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00} }; // ----- Internal helpers (alphabetical) ----- static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg) { const uint8_t *glyph; uint8_t row; uint8_t col; uint8_t bits; uint8_t c; uint8_t *stagePixels; c = (uint8_t)ch; if (c < FIRST_CHAR || c > LAST_CHAR) { c = FIRST_CHAR; // unknown -> space } glyph = kFont[c - FIRST_CHAR]; // Chunky fast path: each glyph row is 8 bits == 4 stage bytes // (each byte = 2 pixels in 4bpp packed). Build each byte by // checking 2 bits at a time. Bypasses jlDrawPixel's per-pixel // overhead which dominates text rendering on slow targets. stagePixels = (stage != NULL) ? stage->pixels : NULL; if (stagePixels != NULL && x >= 0 && y >= 0 && x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH && y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT && (x & 1) == 0) { uint16_t rowOff; uint16_t byteX; uint8_t bytePair; uint8_t left; uint8_t right; byteX = (uint16_t)(x >> 1); for (row = 0u; row < GLYPH_H; row++) { bits = glyph[row]; rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX); for (col = 0u; col < GLYPH_W; col += 2u) { left = (uint8_t)((bits & (uint8_t)(0x80u >> col)) ? fg : bg); right = (uint8_t)((bits & (uint8_t)(0x80u >> (col + 1u))) ? fg : bg); bytePair = (uint8_t)((left << 4) | right); stagePixels[rowOff + (col >> 1)] = bytePair; } } surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H); return; } // Planar / out-of-bounds slow fallback. for (row = 0u; row < GLYPH_H; row++) { bits = glyph[row]; for (col = 0u; col < GLYPH_W; col++) { uint8_t pixel; pixel = ((bits >> (uint8_t)(7u - col)) & 0x01u) ? fg : bg; jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), pixel); } } } // Draw a char with transparent background: only foreground bits are // written, the surface pixels underneath show through. Chunky fast // path reads each stage byte, modifies set nibbles, writes back. static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg) { const uint8_t *glyph; uint8_t row; uint8_t col; uint8_t bits; uint8_t c; uint8_t *stagePixels; c = (uint8_t)ch; if (c < FIRST_CHAR || c > LAST_CHAR) { return; // unknown -> nothing to draw (transparent) } glyph = kFont[c - FIRST_CHAR]; stagePixels = (stage != NULL) ? stage->pixels : NULL; if (stagePixels != NULL && x >= 0 && y >= 0 && x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH && y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT && (x & 1) == 0) { uint16_t rowOff; uint16_t byteX; uint8_t existing; uint8_t leftMask; uint8_t rightMask; uint8_t nibbleFg; byteX = (uint16_t)(x >> 1); nibbleFg = (uint8_t)(fg & 0x0Fu); for (row = 0u; row < GLYPH_H; row++) { bits = glyph[row]; if (bits == 0u) { continue; } // entire row transparent rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX); for (col = 0u; col < GLYPH_W; col += 2u) { leftMask = (uint8_t)(bits & (uint8_t)(0x80u >> col)); rightMask = (uint8_t)(bits & (uint8_t)(0x80u >> (col + 1u))); if (leftMask == 0u && rightMask == 0u) { continue; } existing = stagePixels[rowOff + (col >> 1)]; if (leftMask) { existing = (uint8_t)((existing & 0x0Fu) | (nibbleFg << 4)); } if (rightMask) { existing = (uint8_t)((existing & 0xF0u) | nibbleFg); } stagePixels[rowOff + (col >> 1)] = existing; } } surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H); return; } // Slow fallback (planar or out-of-bounds). for (row = 0u; row < GLYPH_H; row++) { bits = glyph[row]; for (col = 0u; col < GLYPH_W; col++) { if (bits & (uint8_t)(0x80u >> col)) { jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), fg); } } } } static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg) { int16_t cur; if (s == NULL) { return; } cur = x; while (*s != '\0') { if (cur >= (int16_t)STAGE_W) { return; } drawChar(stage, cur, y, *s, fg, bg); cur = (int16_t)(cur + (int16_t)GLYPH_W); s++; } } static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border) { int16_t w; int16_t h; w = (int16_t)((int16_t)cols * (int16_t)GLYPH_W); h = (int16_t)((int16_t)rows * (int16_t)GLYPH_H); jlFillRect(stage, x, y, (uint16_t)(w + 4), (uint16_t)(h + 4), border); jlFillRect(stage, (int16_t)(x + 2), (int16_t)(y + 2), (uint16_t)w, (uint16_t)h, bg); } static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg) { jlFillRect(stage, 0, (int16_t)((int16_t)row * (int16_t)GLYPH_H), (uint16_t)STAGE_W, (uint16_t)GLYPH_H, bg); } // ----- Public API (alphabetical) ----- const uint16_t *agiTextAsciiMap(void) { // Not used in this impl (we render direct), but expose so callers // who want to use jlDrawText against agiTextFontSurface have the // standard 16x6 layout map. Built lazily on first call. static uint16_t map[256]; static bool built; uint16_t i; if (built) { return map; } for (i = 0u; i < 256u; i++) { if (i < FIRST_CHAR || i > LAST_CHAR) { map[i] = (uint16_t)0xFFFFu; } else { uint8_t off; off = (uint8_t)(i - FIRST_CHAR); map[i] = (uint16_t)(((uint16_t)(off / 16u) << 8) | (uint16_t)(off % 16u)); } } built = true; return map; } const jlSurfaceT *agiTextFontSurface(void) { // No surface built in this impl (drawChar walks raw bits direct // to the stage). Reserved for callers that prefer jlDrawText. return NULL; } void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage) { uint8_t r; if (vm->statusLineOn) { fillRow(stage, vm->statusLineRow, 15u); // white background if (vm->textRows[vm->statusLineRow].text[0] != '\0') { drawString(stage, 0, (int16_t)((int16_t)vm->statusLineRow * (int16_t)GLYPH_H), vm->textRows[vm->statusLineRow].text, 0u, 15u); } } // display() rows: walk every row that has content. Glyphs draw // transparently (no bg fill) so any PIC art behind shows through // where glyph pixels are 0. Filling the whole row's bg would // obscure picture elements that overlap the text band (e.g. // KQ3's "King's Quest III" title art). for (r = 0u; r < AGI_TEXT_ROWS; r++) { const char *p; int16_t x; int16_t y; uint8_t fg; if (r == vm->statusLineRow && vm->statusLineOn) { continue; // status line handled above } if (vm->textRows[r].text[0] == '\0') { continue; } y = (int16_t)((int16_t)r * (int16_t)GLYPH_H); x = 0; fg = vm->textRows[r].fg; p = vm->textRows[r].text; while (*p != '\0' && x < (int16_t)STAGE_W) { if (*p != ' ') { drawCharTransparent(stage, x, y, *p, fg); } x = (int16_t)(x + (int16_t)GLYPH_W); p++; } } if (vm->printModal.active) { const char *p; int16_t lineW; int16_t maxW; int16_t lineCount; int16_t winX; int16_t winY; int16_t lineH; int16_t textY; int16_t curX; // Measure: width = longest line in chars; height = lines. lineW = 0; maxW = 1; lineCount = 1; p = vm->printModal.message; while (*p != '\0') { if (*p == '\n') { if (lineW > maxW) { maxW = lineW; } lineW = 0; lineCount++; } else { lineW++; } p++; } if (lineW > maxW) { maxW = lineW; } if (maxW > (int16_t)AGI_TEXT_COLS) { maxW = (int16_t)AGI_TEXT_COLS; } lineH = (int16_t)GLYPH_H; winX = (int16_t)((STAGE_W - (maxW * (int16_t)GLYPH_W + 4)) / 2); winY = (int16_t)((STAGE_H - (lineCount * lineH + 4)) / 2); if (winX < 0) { winX = 0; } if (winY < 0) { winY = 0; } drawWindow(stage, winX, winY, (uint8_t)maxW, (uint8_t)lineCount, 15u, 4u); textY = (int16_t)(winY + 2); curX = (int16_t)(winX + 2); p = vm->printModal.message; while (*p != '\0') { if (*p == '\n') { textY = (int16_t)(textY + lineH); curX = (int16_t)(winX + 2); } else { drawChar(stage, curX, textY, *p, 0u, 15u); curX = (int16_t)(curX + (int16_t)GLYPH_W); } p++; } } }