// Text: display(), message windows, the status and input lines, menus, // the inventory screen and show.obj (AGI specification 4.2's text, menu // and inventory commands). // // Text sits on the 40x25 grid of 8x8 cells, drawn straight onto the // stage. In graphics mode Sierra's interpreter draws only white-on-black // style text: a set.text.attribute background other than black shows as // black on white. A message window is the wrapped text (30 columns at // most) on white, inside a red frame, centred over the picture unless // print.at places it; the screen under it is saved and put back when it // closes. Geometry measured from Sierra's interpreter. #include "agi.h" #include #include #include #include "joey/core.h" #include "joey/draw.h" #include "joey/tile.h" #include "surfaceInternal.h" #define WINDOW_MAX_COLS 30u #define WINDOW_MAX_LINES 20u #define WRAP_NO_SPACE 0xFFu // A window's box, in AGI pixels across and screen rows down: it starts 5 // pixels left of and 5 rows above the text and is 10 pixels wider and 10 // rows taller; a red frame runs one pixel (one row) in from the edge. #define WINDOW_BOX_LEFT 5 #define WINDOW_BOX_EXTRA_W 10 #define WINDOW_BOX_TOP 5 #define WINDOW_BOX_EXTRA_H 10 #define WINDOW_FRAME_IN 1 #define AGI_PX_PER_CELL (AGI_CELL_PX / AGI_SCREEN_PX_PER_AGI) #define SCREEN_ROWS ((int16_t)AGI_TEXT_ROWS * AGI_CELL_PX) #define SCREEN_CELLS ((int16_t)(AGI_TEXT_ROWS * AGI_TEXT_COLS)) // jlSurfaceCopyRect copies whole groups of this many screen pixels. #define COPY_GROUP_PX 16 // Menu boxes, in screen pixels: the frame line two pixels in at the // sides and one row in at the top and bottom. #define MENU_LINE_X 2 #define MENU_LINE_Y 1 #define MENU_LINE_W 2 #define MENU_LINE_H 1 #define WINDOW_TEXT_FG AGI_COLOR_BLACK #define WINDOW_BG AGI_COLOR_WHITE #define WINDOW_FRAME AGI_COLOR_RED #define STATUS_SCORE_COL 1u #define STATUS_SOUND_COL 30u #define FORMAT_MAX 8u #define NUMBER_MAX_DIGITS 3u #define DECIMAL_BASE 10u #define EXPAND_MAX 512u #define EXPAND_DEPTH_MAX 4u // The inventory lists two items a row: the first at column 1, the second // ending at column 38 (measured). #define INVENTORY_COLS 2u #define INVENTORY_LEFT_COL 1u #define INVENTORY_RIGHT_END 39u #define INVENTORY_FIRST_ROW 2u // The hint lines' columns, as Sierra's interpreter places them. #define INVENTORY_HINT_COL 4u #define INVENTORY_PICK_COL 2u #define MENU_ROW 0u #define MENU_FIRST_COL 1u #define MENU_ITEM_ROW 2u #define TEXT_ATTR_BG_SHIFT 4u #define MENU_MARGIN 8 #define MENU_FRAME AGI_COLOR_BLACK #define DOT_MASK_EVEN_ROW 0xAAu #define DOT_MASK_ODD_ROW 0x55u #define SHOW_OBJ_MARGIN 4 // What windowSpan does to its pixels. typedef enum { SPAN_FILL = 0, SPAN_SAVE = 1, SPAN_RESTORE = 2 } SpanOpE; // ----- Prototypes ----- static void attrColors(uint8_t *fg, uint8_t *bg); static uint8_t centerCol(const char *text); static uint8_t clearColor(uint8_t color); static void copySpan(jlSurfaceT *dst, const jlSurfaceT *src, int16_t col, int16_t row, int16_t run); static void drawBox(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t frame); static uint8_t drawMenuBar(uint8_t current); static void drawMenuItems(uint8_t menu, uint8_t current); static void drawText(uint8_t row, uint8_t col, const char *text, uint8_t fg, uint8_t bg); static void echoCell(char ch, uint8_t fg, uint8_t bg); static void expandInto(const char *src, char *dst, uint16_t dstCap, uint16_t *pos, uint8_t depth); static void openWindow(int16_t x, int16_t y, int16_t w, int16_t h); static void putCell(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg); static void putGlyph(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg, bool dotted); static void putNumber(char *dst, uint16_t dstCap, uint16_t *pos, uint8_t value, uint8_t width); static void spanOp(int16_t col, int16_t row, int16_t run, SpanOpE op, uint8_t color); static void windowBox(SpanOpE op, uint8_t color); static void windowSpan(int16_t x, int16_t row, int16_t len, SpanOpE op, uint8_t color); static uint8_t wrapText(const char *text, uint8_t maxCols, char lines[][AGI_TEXT_COLS + 1u], uint8_t maxLines, uint8_t *outWidth); // ----- Strings ----- static const char kCarrying[] = "You are carrying:"; static const char kNothing[] = "nothing"; // ----- Module state ----- // The open window: its save-under and its box (AGI pixels, screen rows). static jlSurfaceT *gWinSave; static int16_t gWinX; static int16_t gWinY; static int16_t gWinW; static int16_t gWinH; // Wrapped text on its way to the screen (print and display draw it at once). static char gLines[WINDOW_MAX_LINES][AGI_TEXT_COLS + 1u]; // Where the command line's echo draws next, as row * 40 + column. static int16_t gEchoPos; // What the status line shows, to redraw it only when that changes. static uint8_t gShownScore; static bool gShownSound; // ----- Internal helpers (alphabetical) ----- // The text attribute's colours. On the text screen they are used as they // are; on the picture a background makes the text black on white. static void attrColors(uint8_t *fg, uint8_t *bg) { if (gAgi->textMode) { *fg = gAgi->textFg; *bg = gAgi->textBg; return; } if (gAgi->textBg != AGI_COLOR_BLACK) { *fg = AGI_COLOR_BLACK; *bg = AGI_COLOR_WHITE; } else { *fg = gAgi->textFg; *bg = AGI_COLOR_BLACK; } } // The column that centres a line of text on the screen. static uint8_t centerCol(const char *text) { size_t len; len = strlen(text); return (uint8_t)((len >= AGI_TEXT_COLS) ? 0u : (AGI_TEXT_COLS - len) / 2u); } // clear.lines and clear.text.rect: on the picture any colour but black // clears to white; on the text screen the colour is a PC text attribute // whose high nibble is the background. static uint8_t clearColor(uint8_t color) { if (gAgi->textMode) { return (uint8_t)((color >> TEXT_ATTR_BG_SHIFT) & AGI_COLOR_MASK); } return (color == AGI_COLOR_BLACK) ? AGI_COLOR_BLACK : AGI_COLOR_WHITE; } // run AGI pixels of one screen row from src to dst, and no more (a // window's own text and frame can sit right beside a span it puts back). // On a chunky surface an AGI pixel is one byte; a planar one copies the // whole groups inside the run with jlSurfaceCopyRect, which would widen // the run to its groups, and the pixels at its ends one by one. static void copySpan(jlSurfaceT *dst, const jlSurfaceT *src, int16_t col, int16_t row, int16_t run) { int16_t x; int16_t xEnd; int16_t inner; int16_t innerEnd; uint16_t at; x = (int16_t)(col * AGI_SCREEN_PX_PER_AGI); xEnd = (int16_t)(x + run * AGI_SCREEN_PX_PER_AGI); if (dst->pixels != NULL && src->pixels != NULL) { at = (uint16_t)((uint16_t)row * SURFACE_BYTES_PER_ROW + (uint16_t)col); memcpy(&dst->pixels[at], &src->pixels[at], (size_t)run); surfaceMarkDirtyRect(dst, x, row, (int16_t)(xEnd - x), 1); return; } inner = (int16_t)((x + COPY_GROUP_PX - 1) & ~(COPY_GROUP_PX - 1)); innerEnd = (int16_t)(xEnd & ~(COPY_GROUP_PX - 1)); if (inner >= innerEnd) { inner = xEnd; innerEnd = xEnd; } else { jlSurfaceCopyRect(dst, src, inner, row, (uint16_t)(innerEnd - inner), 1u); } for (; x < inner; x++) { jlDrawPixel(dst, x, row, jlSamplePixel(src, x, row)); } for (x = innerEnd; x < xEnd; x++) { jlDrawPixel(dst, x, row, jlSamplePixel(src, x, row)); } } // A menu's white box with its frame line just inside the edge. static void drawBox(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t frame) { jlSurfaceT *stage; stage = jlStageGet(); jlFillRect(stage, x, y, w, h, WINDOW_BG); jlFillRect(stage, (int16_t)(x + MENU_LINE_X), (int16_t)(y + MENU_LINE_Y), (uint16_t)(w - 2u * MENU_LINE_X), MENU_LINE_H, frame); jlFillRect(stage, (int16_t)(x + MENU_LINE_X), (int16_t)(y + (int16_t)h - MENU_LINE_Y - MENU_LINE_H), (uint16_t)(w - 2u * MENU_LINE_X), MENU_LINE_H, frame); jlFillRect(stage, (int16_t)(x + MENU_LINE_X), (int16_t)(y + MENU_LINE_Y), MENU_LINE_W, (uint16_t)(h - 2u * MENU_LINE_Y), frame); jlFillRect(stage, (int16_t)(x + (int16_t)w - MENU_LINE_X - MENU_LINE_W), (int16_t)(y + MENU_LINE_Y), MENU_LINE_W, (uint16_t)(h - 2u * MENU_LINE_Y), frame); } // The menu bar across the top row, menu `current` highlighted. Returns // the number of menus. static uint8_t drawMenuBar(uint8_t current) { uint8_t m; uint8_t col; jlFillRect(jlStageGet(), 0, (int16_t)(MENU_ROW * AGI_CELL_PX), (uint16_t)(AGI_TEXT_COLS * AGI_CELL_PX), AGI_CELL_PX, AGI_COLOR_WHITE); col = MENU_FIRST_COL; for (m = 0u; m < gAgi->menuCount; m++) { gAgi->menus[m].col = col; if (m == current) { drawText(MENU_ROW, col, gAgi->menus[m].name, AGI_COLOR_WHITE, AGI_COLOR_BLACK); } else { drawText(MENU_ROW, col, gAgi->menus[m].name, AGI_COLOR_BLACK, AGI_COLOR_WHITE); } col = (uint8_t)(col + strlen(gAgi->menus[m].name) + 1u); } return gAgi->menuCount; } // A menu's items in a black-framed box under its name, item `current` // highlighted and disabled items dotted. Sierra's interpreter moves the // box left when it would run off the screen. static void drawMenuItems(uint8_t menu, uint8_t current) { const AgiMenuT *m; uint8_t i; uint8_t width; uint8_t col; m = &gAgi->menus[menu]; width = 0u; for (i = 0u; i < m->itemCount; i++) { uint8_t len; len = (uint8_t)strlen(gAgi->menuItems[m->firstItem + i].text); if (len > width) { width = len; } } col = m->col; if ((uint16_t)col + width + 1u > AGI_TEXT_COLS) { col = (uint8_t)(AGI_TEXT_COLS - width - 1u); } drawBox((int16_t)((int16_t)col * AGI_CELL_PX - MENU_MARGIN), (int16_t)((int16_t)MENU_ITEM_ROW * AGI_CELL_PX - MENU_MARGIN), (uint16_t)((uint16_t)width * AGI_CELL_PX + 2u * MENU_MARGIN), (uint16_t)((uint16_t)m->itemCount * AGI_CELL_PX + 2u * MENU_MARGIN), MENU_FRAME); for (i = 0u; i < m->itemCount; i++) { const AgiMenuItemT *it = &gAgi->menuItems[m->firstItem + i]; const char *s; uint8_t c; bool hi; hi = (i == current); c = col; for (s = it->text; *s != '\0' && c < AGI_TEXT_COLS; s++, c++) { putGlyph((uint8_t)(MENU_ITEM_ROW + i), c, *s, hi ? AGI_COLOR_WHITE : AGI_COLOR_BLACK, hi ? AGI_COLOR_BLACK : AGI_COLOR_WHITE, !it->enabled); } } } static void drawText(uint8_t row, uint8_t col, const char *text, uint8_t fg, uint8_t bg) { while (*text != '\0' && col < AGI_TEXT_COLS) { putCell(row, col, *text, fg, bg); col++; text++; } } // One character of the command line's echo at gEchoPos, which moves on; // a position off the screen draws nothing. static void echoCell(char ch, uint8_t fg, uint8_t bg) { if (gEchoPos >= 0 && gEchoPos < SCREEN_CELLS) { putCell((uint8_t)(gEchoPos / AGI_TEXT_COLS), (uint8_t)(gEchoPos % AGI_TEXT_COLS), ch, fg, bg); } gEchoPos++; } // Expand a message's format codes into dst at *pos. static void expandInto(const char *src, char *dst, uint16_t dstCap, uint16_t *pos, uint8_t depth) { char code; uint8_t arg; uint8_t width; bool haveArg; while (*src != '\0' && *pos + 1u < dstCap) { if (*src == '\\') { // A backslash takes the next character literally. src++; if (*src != '\0') { dst[(*pos)++] = *src++; } continue; } if (*src != '%') { dst[(*pos)++] = *src++; continue; } src++; code = *src; if (code == '\0') { break; } src++; arg = 0u; haveArg = false; while (*src >= '0' && *src <= '9') { arg = (uint8_t)(arg * DECIMAL_BASE + (uint8_t)(*src - '0')); haveArg = true; src++; } // A '%' with no number after its letter shows nothing. if (!haveArg) { continue; } switch (code) { case 'v': width = 0u; if (*src == '|') { src++; while (*src >= '0' && *src <= '9') { width = (uint8_t)(width * DECIMAL_BASE + (uint8_t)(*src - '0')); src++; } } putNumber(dst, dstCap, pos, gAgi->vars[arg], width); break; case 's': if (arg < AGI_MAX_STRINGS) { expandInto(gAgi->strings[arg], dst, dstCap, pos, EXPAND_DEPTH_MAX); } break; case 'w': if (arg >= 1u && arg <= gAgi->wordCount) { expandInto(gAgi->wordText[arg - 1u], dst, dstCap, pos, EXPAND_DEPTH_MAX); } break; case '0': expandInto(agiItemName(&gAgi->game, arg), dst, dstCap, pos, EXPAND_DEPTH_MAX); break; case 'm': if (depth < EXPAND_DEPTH_MAX) { expandInto(vmMessage(gAgi->currentLogic, arg), dst, dstCap, pos, (uint8_t)(depth + 1u)); } break; case 'g': if (depth < EXPAND_DEPTH_MAX) { expandInto(vmMessage(0u, arg), dst, dstCap, pos, (uint8_t)(depth + 1u)); } break; default: break; } } dst[*pos] = '\0'; } // Save the stage under a window and draw its white box and red frame // (x and w in AGI pixels, y and h in screen rows). static void openWindow(int16_t x, int16_t y, int16_t w, int16_t h) { int16_t r; if (gAgi->windowOpen) { textCloseWindow(); } if (gWinSave == NULL) { gWinSave = jlSurfaceCreate(); } gWinX = x; gWinY = y; gWinW = w; gWinH = h; if (gWinSave != NULL) { windowBox(SPAN_SAVE, 0u); } gAgi->windowOpen = true; windowBox(SPAN_FILL, WINDOW_BG); windowSpan((int16_t)(x + WINDOW_FRAME_IN), (int16_t)(y + WINDOW_FRAME_IN), (int16_t)(w - 2 * WINDOW_FRAME_IN), SPAN_FILL, WINDOW_FRAME); windowSpan((int16_t)(x + WINDOW_FRAME_IN), (int16_t)(y + h - 1 - WINDOW_FRAME_IN), (int16_t)(w - 2 * WINDOW_FRAME_IN), SPAN_FILL, WINDOW_FRAME); // The sides stop a row short of the top and bottom lines (measured). for (r = (int16_t)(y + WINDOW_FRAME_IN + 1); r < y + h - WINDOW_FRAME_IN - 1; r++) { windowSpan((int16_t)(x + WINDOW_FRAME_IN), r, 1, SPAN_FILL, WINDOW_FRAME); windowSpan((int16_t)(x + w - 1 - WINDOW_FRAME_IN), r, 1, SPAN_FILL, WINDOW_FRAME); } } static void putCell(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg) { putGlyph(row, col, ch, fg, bg, false); } // One character cell; dotted keeps only every other pixel of the glyph, // as disabled menu items show. static void putGlyph(uint8_t row, uint8_t col, char ch, uint8_t fg, uint8_t bg, bool dotted) { uint8_t c; uint8_t bits[AGI_FONT_ROWS]; uint8_t r; if (row >= AGI_TEXT_ROWS || col >= AGI_TEXT_COLS) { return; } c = (uint8_t)ch; if (c < AGI_FONT_FIRST || c >= AGI_FONT_FIRST + AGI_FONT_GLYPHS) { c = AGI_FONT_FIRST; } for (r = 0u; r < AGI_FONT_ROWS; r++) { bits[r] = kAgiFont[c - AGI_FONT_FIRST][r]; if (dotted) { bits[r] = (uint8_t)(bits[r] & (((r & 1u) != 0u) ? DOT_MASK_ODD_ROW : DOT_MASK_EVEN_ROW)); } } jlTilePasteGlyph(jlStageGet(), col, row, bits, fg, bg); } // A variable's value in decimal; a width pads it with leading zeros. static void putNumber(char *dst, uint16_t dstCap, uint16_t *pos, uint8_t value, uint8_t width) { char digits[NUMBER_MAX_DIGITS]; uint8_t n; n = 0u; do { digits[n++] = (char)('0' + value % DECIMAL_BASE); value = (uint8_t)(value / DECIMAL_BASE); } while (value != 0u); while (width > n && *pos + 1u < dstCap) { dst[(*pos)++] = '0'; width--; } while (n > 0u && *pos + 1u < dstCap) { n--; dst[(*pos)++] = digits[n]; } } // One run of a window span on one screen row: AGI pixels col.. on row. static void spanOp(int16_t col, int16_t row, int16_t run, SpanOpE op, uint8_t color) { switch (op) { case SPAN_FILL: jlFillRect(jlStageGet(), (int16_t)(col * AGI_SCREEN_PX_PER_AGI), row, (uint16_t)(run * AGI_SCREEN_PX_PER_AGI), 1u, color); break; case SPAN_SAVE: copySpan(gWinSave, jlStageGet(), col, row, run); break; case SPAN_RESTORE: copySpan(jlStageGet(), gWinSave, col, row, run); break; default: break; } } // The open window's whole box, row by row. static void windowBox(SpanOpE op, uint8_t color) { int16_t r; for (r = 0; r < gWinH; r++) { windowSpan(gWinX, (int16_t)(gWinY + r), gWinW, op, color); } } // len AGI pixels of a window from (x, row). Sierra's interpreter // (AGI_QUIRK_WINDOW_BYTE_X) keeps the x in a byte and walks the screen as // one line of 160-pixel rows, so a box that starts left of the screen // starts 256 pixels on (a row down, 96 pixels in) and a span runs on into // the next row; elsewhere spans are just clipped to the screen. static void windowSpan(int16_t x, int16_t row, int16_t len, SpanOpE op, uint8_t color) { int32_t addr; int32_t end; int16_t run; int16_t col; if (len <= 0) { return; } if (!agiQuirk(AGI_QUIRK_WINDOW_BYTE_X)) { if (x < 0) { len = (int16_t)(len + x); x = 0; } if (x + len > AGI_WIDTH) { len = (int16_t)(AGI_WIDTH - x); } if (len > 0 && row >= 0 && row < SCREEN_ROWS) { spanOp(x, row, len, op, color); } return; } addr = (int32_t)row * AGI_WIDTH + (uint8_t)x; end = addr + len; while (addr < end) { col = (int16_t)(addr % AGI_WIDTH); run = (int16_t)((end - addr < AGI_WIDTH - col) ? end - addr : AGI_WIDTH - col); if (addr >= 0 && addr / AGI_WIDTH < SCREEN_ROWS) { spanOp(col, (int16_t)(addr / AGI_WIDTH), run, op, color); } addr += run; } } // Word-wrap text as Sierra's interpreter does: when a line reaches // maxCols characters it breaks at its last space (so a wrapped line holds // at most maxCols - 1), or, with no space, right there; a newline breaks // too. Returns the line count and the widest line in *outWidth. static uint8_t wrapText(const char *text, uint8_t maxCols, char lines[][AGI_TEXT_COLS + 1u], uint8_t maxLines, uint8_t *outWidth) { uint8_t count; uint8_t len; uint8_t lastSpace; uint8_t carry; char c; count = 0u; len = 0u; lastSpace = WRAP_NO_SPACE; c = '\0'; *outWidth = 0u; while (*text != '\0' && count < maxLines) { c = *text++; if (c == '\n') { lines[count][len] = '\0'; if (len > *outWidth) { *outWidth = len; } count++; len = 0u; lastSpace = WRAP_NO_SPACE; continue; } lines[count][len++] = c; if (c == ' ') { lastSpace = (uint8_t)(len - 1u); } if (len < maxCols) { continue; } carry = 0u; if (lastSpace != WRAP_NO_SPACE) { carry = (uint8_t)(len - lastSpace - 1u); len = lastSpace; } lines[count][len] = '\0'; if (len > *outWidth) { *outWidth = len; } count++; if (count >= maxLines) { break; } if (carry > 0u) { memcpy(lines[count], &lines[count - 1u][lastSpace + 1u], carry); } len = carry; lastSpace = WRAP_NO_SPACE; } // A text ending in a newline still has its empty last line. if (count < maxLines && (len > 0u || count == 0u || c == '\n')) { lines[count][len] = '\0'; if (len > *outWidth) { *outWidth = len; } count++; } return count; } // ----- Public API (alphabetical) ----- void textClearLines(uint8_t top, uint8_t bottom, uint8_t color) { if (top >= AGI_TEXT_ROWS) { return; } if (bottom >= AGI_TEXT_ROWS) { bottom = (uint8_t)(AGI_TEXT_ROWS - 1u); } if (bottom < top) { return; } color = clearColor(color); jlFillRect(jlStageGet(), 0, (int16_t)((int16_t)top * AGI_CELL_PX), (uint16_t)(AGI_TEXT_COLS * AGI_CELL_PX), (uint16_t)((uint16_t)(bottom - top + 1u) * AGI_CELL_PX), color); } void textClearRect(uint8_t top, uint8_t left, uint8_t bottom, uint8_t right, uint8_t color) { if (top >= AGI_TEXT_ROWS || left >= AGI_TEXT_COLS || bottom < top || right < left) { return; } if (bottom >= AGI_TEXT_ROWS) { bottom = (uint8_t)(AGI_TEXT_ROWS - 1u); } if (right >= AGI_TEXT_COLS) { right = (uint8_t)(AGI_TEXT_COLS - 1u); } color = clearColor(color); jlFillRect(jlStageGet(), (int16_t)((int16_t)left * AGI_CELL_PX), (int16_t)((int16_t)top * AGI_CELL_PX), (uint16_t)((uint16_t)(right - left + 1u) * AGI_CELL_PX), (uint16_t)((uint16_t)(bottom - top + 1u) * AGI_CELL_PX), color); } void textCloseWindow(void) { if (!gAgi->windowOpen) { return; } gAgi->windowOpen = false; if (gWinSave != NULL) { windowBox(SPAN_RESTORE, 0u); } } // display(): text at a row and column in the current attribute, wrapped // like a window's at the right edge; further lines start at the same // column. void textDisplay(uint8_t row, uint8_t col, const char *text) { uint8_t fg; uint8_t bg; uint8_t count; uint8_t width; uint8_t i; if (col >= AGI_TEXT_COLS) { return; } attrColors(&fg, &bg); count = wrapText(text, (uint8_t)(AGI_TEXT_COLS - col), gLines, WINDOW_MAX_LINES, &width); for (i = 0u; i < count && row < AGI_TEXT_ROWS; i++) { drawText(row, col, gLines[i], fg, bg); row++; } } // The command line: the prompt (string 0), what has been typed and the // cursor, on the input row. void textDrawInputLine(void) { uint8_t fg; uint8_t bg; uint8_t col; uint8_t i; if (gAgi->textMode) { return; } attrColors(&fg, &bg); jlFillRect(jlStageGet(), 0, (int16_t)((int16_t)gAgi->inputRow * AGI_CELL_PX), (uint16_t)(AGI_TEXT_COLS * AGI_CELL_PX), AGI_CELL_PX, bg); if (!gAgi->inputOn) { return; } col = 0u; for (i = 0u; gAgi->strings[0][i] != '\0' && col < AGI_TEXT_COLS; i++) { putCell(gAgi->inputRow, col++, gAgi->strings[0][i], fg, bg); } for (i = 0u; i < gAgi->inputLen && col < AGI_TEXT_COLS; i++) { putCell(gAgi->inputRow, col++, gAgi->inputLine[i], fg, bg); } if (col < AGI_TEXT_COLS && gAgi->cursorChar != AGI_NO_CURSOR) { putCell(gAgi->inputRow, col++, gAgi->cursorChar, fg, bg); } gEchoPos = (int16_t)((int16_t)gAgi->inputRow * AGI_TEXT_COLS + col); } // The status line: the score at the left and whether sound is on. void textDrawStatus(void) { char buf[AGI_TEXT_COLS + 1u]; if (gAgi->textMode || !gAgi->statusOn) { return; } jlFillRect(jlStageGet(), 0, (int16_t)((int16_t)gAgi->statusRow * AGI_CELL_PX), (uint16_t)(AGI_TEXT_COLS * AGI_CELL_PX), AGI_CELL_PX, AGI_COLOR_WHITE); snprintf(buf, sizeof(buf), "Score:%u of %u", (unsigned)gAgi->vars[VAR_SCORE], (unsigned)gAgi->vars[VAR_MAX_SCORE]); drawText(gAgi->statusRow, STATUS_SCORE_COL, buf, AGI_COLOR_BLACK, AGI_COLOR_WHITE); snprintf(buf, sizeof(buf), "Sound:%s", gAgi->flags[FLAG_SOUND] ? "on" : "off"); drawText(gAgi->statusRow, STATUS_SOUND_COL, buf, AGI_COLOR_BLACK, AGI_COLOR_WHITE); gShownScore = gAgi->vars[VAR_SCORE]; gShownSound = gAgi->flags[FLAG_SOUND] != 0u; } // One edit of the command line, drawn as Sierra's interpreter draws it: // only the cell typed or rubbed out and the cursor change, so anything // else on the input row stays until the line is redrawn (Enter, // accept.input). The echo goes on from where the last one left off, // stepping back over the cursor first when there is a cursor: with // AGI_QUIRK_ECHO_STEPS_BACK it does so even when the cursor was set after // the line was drawn, and never drawn (measured on 2.435: a cursor set // mid-line makes the next character overwrite the last one, and rubbing // out can run back over the prompt); otherwise the echo starts from // the end of what is typed. void textEchoInput(uint8_t oldLen) { uint8_t fg; uint8_t bg; bool cursor; if (gAgi->textMode || !gAgi->inputOn || oldLen == gAgi->inputLen) { return; } attrColors(&fg, &bg); cursor = (gAgi->cursorChar != AGI_NO_CURSOR); if (!agiQuirk(AGI_QUIRK_ECHO_STEPS_BACK)) { gEchoPos = (int16_t)((int16_t)gAgi->inputRow * AGI_TEXT_COLS + (int16_t)strlen(gAgi->strings[0]) + oldLen + (cursor ? 1 : 0)); } if (gAgi->inputLen > oldLen) { if (cursor) { gEchoPos--; } echoCell(gAgi->inputLine[oldLen], fg, bg); if (cursor) { echoCell(gAgi->cursorChar, fg, bg); } return; } if (cursor) { gEchoPos = (int16_t)(gEchoPos - 2); echoCell(gAgi->cursorChar, fg, bg); } else { gEchoPos--; } echoCell(' ', fg, bg); gEchoPos--; } // Expand a message's format codes: %vN (a variable, %vN|W zero-padded to // W digits), %sN (a string), %wN (the Nth word typed), %0N (an inventory // item's name), %mN (a message of the running logic), %gN (a message of // logic 0). void textExpand(const char *src, char *dst, uint16_t dstCap) { uint16_t pos; pos = 0u; if (dstCap == 0u) { return; } expandInto(src, dst, dstCap, &pos, 0u); } // graphics(): back from the text screen to the picture and its lines. void textGraphicsMode(void) { gAgi->textMode = false; jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK); gfxRestorePicture(); textDrawStatus(); textDrawInputLine(); } // status(): the inventory on a text screen, two columns of the items // carried; with f13 set one can be picked into v25. void textInventory(void) { uint8_t carried[AGI_MAX_ITEMS]; uint8_t count; uint16_t i; uint8_t sel; uint8_t row; uint8_t col; uint16_t key; bool selecting; bool wasText; count = 0u; for (i = 0u; i < gAgi->game.itemCount; i++) { if (gAgi->itemRoom[i] == AGI_ITEM_CARRIED) { carried[count++] = (uint8_t)i; } } wasText = gAgi->textMode; gAgi->textMode = true; jlSurfaceClear(jlStageGet(), AGI_COLOR_WHITE); drawText(0u, centerCol(kCarrying), kCarrying, AGI_COLOR_BLACK, AGI_COLOR_WHITE); selecting = gAgi->flags[FLAG_ITEM_SELECT] != 0u && count > 0u; sel = 0u; for (;;) { if (count == 0u) { drawText(INVENTORY_FIRST_ROW, centerCol(kNothing), kNothing, AGI_COLOR_BLACK, AGI_COLOR_WHITE); } for (i = 0u; i < count; i++) { const char *name = agiItemName(&gAgi->game, carried[i]); bool hi = selecting && i == sel; row = (uint8_t)(INVENTORY_FIRST_ROW + i / INVENTORY_COLS); col = (uint8_t)((i % INVENTORY_COLS == 0u) ? INVENTORY_LEFT_COL : INVENTORY_RIGHT_END - strlen(name)); drawText(row, col, name, hi ? AGI_COLOR_WHITE : AGI_COLOR_BLACK, hi ? AGI_COLOR_BLACK : AGI_COLOR_WHITE); } drawText((uint8_t)(AGI_TEXT_ROWS - 1u), selecting ? INVENTORY_PICK_COL : INVENTORY_HINT_COL, selecting ? "Press ENTER to select, ESC to cancel" : "Press a key to return to the game", AGI_COLOR_BLACK, AGI_COLOR_WHITE); key = inputWaitKey(); if (!selecting) { break; } if (key == AGI_KEY_ENTER) { gAgi->vars[VAR_SELECTED_ITEM] = carried[sel]; break; } if (key == AGI_KEY_ESCAPE) { gAgi->vars[VAR_SELECTED_ITEM] = AGI_ITEM_CARRIED; break; } if (key == AGI_KEY_RIGHT && sel + 1u < count) { sel++; } else if (key == AGI_KEY_LEFT && sel > 0u) { sel--; } else if (key == AGI_KEY_DOWN && sel + INVENTORY_COLS < count) { sel = (uint8_t)(sel + INVENTORY_COLS); } else if (key == AGI_KEY_UP && sel >= INVENTORY_COLS) { sel = (uint8_t)(sel - INVENTORY_COLS); } } gAgi->textMode = wasText; if (!wasText) { textGraphicsMode(); } } void textMenuAdd(const char *name) { AgiMenuT *m; if (gAgi->menuCount >= AGI_MAX_MENUS || gAgi->menuSubmitted) { return; } m = &gAgi->menus[gAgi->menuCount]; strncpy(m->name, name, AGI_MENU_NAME_MAX); m->name[AGI_MENU_NAME_MAX] = '\0'; m->firstItem = gAgi->menuItemCount; m->itemCount = 0u; gAgi->menuCount++; } void textMenuAddItem(const char *name, uint8_t controller) { AgiMenuItemT *it; if (gAgi->menuCount == 0u || gAgi->menuItemCount >= AGI_MAX_MENU_ITEMS || gAgi->menuSubmitted) { return; } it = &gAgi->menuItems[gAgi->menuItemCount]; strncpy(it->text, name, AGI_MENU_ITEM_MAX); it->text[AGI_MENU_ITEM_MAX] = '\0'; it->controller = controller; it->enabled = true; gAgi->menuItemCount++; gAgi->menus[gAgi->menuCount - 1u].itemCount++; } // menu.input: let the player pick from the menus; a chosen, enabled // item's controller fires. Returns the controller, or 0xFF for none. uint8_t textMenuInput(void) { uint8_t menu; uint8_t item; uint16_t key; uint8_t result; jlSurfaceT *stage; if (gAgi->menuCount == 0u) { return AGI_NO_CONTROLLER; } stage = jlStageGet(); if (gWinSave == NULL) { gWinSave = jlSurfaceCreate(); } if (gWinSave != NULL) { jlSurfaceCopy(gWinSave, stage); } menu = gAgi->menuLastMenu; item = 0u; result = AGI_NO_CONTROLLER; if (menu >= gAgi->menuCount) { menu = 0u; } for (;;) { if (gWinSave != NULL) { jlSurfaceCopy(stage, gWinSave); } (void)drawMenuBar(menu); drawMenuItems(menu, item); key = inputWaitKey(); if (key == AGI_KEY_ESCAPE) { break; } if (key == AGI_KEY_ENTER) { const AgiMenuItemT *it = &gAgi->menuItems[gAgi->menus[menu].firstItem + item]; if (it->enabled) { result = it->controller; break; } } else if (key == AGI_KEY_LEFT) { menu = (menu == 0u) ? (uint8_t)(gAgi->menuCount - 1u) : (uint8_t)(menu - 1u); item = 0u; } else if (key == AGI_KEY_RIGHT) { menu = (uint8_t)((menu + 1u) % gAgi->menuCount); item = 0u; } else if (key == AGI_KEY_UP) { item = (item == 0u) ? (uint8_t)(gAgi->menus[menu].itemCount - 1u) : (uint8_t)(item - 1u); } else if (key == AGI_KEY_DOWN) { item = (uint8_t)((item + 1u) % gAgi->menus[menu].itemCount); } } gAgi->menuLastMenu = menu; if (gWinSave != NULL) { jlSurfaceCopy(stage, gWinSave); } return result; } // A message window the player dismisses (or that times out). void textMessageBox(const char *text) { textPrint(text, -1, -1, 0u); (void)inputWaitPrintClose(0u); textCloseWindow(); } // Open a message window: centred over the picture when row and col are // negative (print), else with its text at (row, col) of the picture area // (print.at). width 0 means the default 30 columns. void textPrint(const char *text, int16_t row, int16_t col, uint8_t width) { uint8_t count; uint8_t maxWidth; uint8_t i; if (width == 0u) { width = WINDOW_MAX_COLS; } else if (width > AGI_TEXT_COLS) { width = AGI_TEXT_COLS; } count = wrapText(text, width, gLines, WINDOW_MAX_LINES, &maxWidth); if (maxWidth == 0u) { maxWidth = 1u; } if (col < 0) { col = (int16_t)((AGI_TEXT_COLS - maxWidth) / 2u); } if (row < 0) { row = (int16_t)((AGI_HEIGHT / AGI_CELL_PX - count) / 2u); } row = (int16_t)(row + gAgi->picRow); openWindow((int16_t)(col * AGI_PX_PER_CELL - WINDOW_BOX_LEFT), (int16_t)(row * AGI_CELL_PX - WINDOW_BOX_TOP), (int16_t)((int16_t)maxWidth * AGI_PX_PER_CELL + WINDOW_BOX_EXTRA_W), (int16_t)((int16_t)count * AGI_CELL_PX + WINDOW_BOX_EXTRA_H)); for (i = 0u; i < count; i++) { drawText((uint8_t)(row + i), (uint8_t)col, gLines[i], WINDOW_TEXT_FG, WINDOW_BG); } } // set.text.attribute. Set on the picture, a background only says "white" // (the text screen then clears to white and shows the foreground on it); // set on the text screen the colours are taken as they are. void textSetAttribute(uint8_t fg, uint8_t bg) { gAgi->textFg = (uint8_t)(fg & AGI_COLOR_MASK); gAgi->textBg = (uint8_t)(bg & AGI_COLOR_MASK); if (!gAgi->textMode && gAgi->textBg != AGI_COLOR_BLACK) { gAgi->textBg = AGI_COLOR_WHITE; } } // show.obj: an inventory item's close-up view at the bottom of the // picture with its description, until a key. void textShowObject(uint8_t viewId) { const AgiViewT *view; char text[EXPAND_MAX]; view = agiViewGet(viewId); if (view == NULL) { return; } if (view->description != NULL) { textExpand(view->description, text, sizeof(text)); } else { text[0] = '\0'; } gfxShowObjectCel(viewId); textPrint(text, -1, -1, 0u); (void)inputWaitPrintClose(0u); textCloseWindow(); gfxRestorePicture(); } // text.screen(): the 40x25 text screen in the attribute's background. void textTextMode(void) { textCloseWindow(); gAgi->textMode = true; jlSurfaceClear(jlStageGet(), gAgi->textBg); } // The status line again if the score or the sound setting has changed. void textUpdateStatus(void) { if (gShownScore != gAgi->vars[VAR_SCORE] || gShownSound != (gAgi->flags[FLAG_SOUND] != 0u)) { textDrawStatus(); } }