1029 lines
34 KiB
C
1029 lines
34 KiB
C
// 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 <stddef.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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();
|
|
}
|
|
}
|