// The keyboard (and joystick) as AGI sees it, the command line and the // parser (AGI specification 3.8 and 4.2's input commands). // // Keys become the 16-bit codes Sierra's interpreter uses: the character // in the low byte, or for keys without one the PC scan code in the high // byte (F1 is 0x3B00, Alt+D 0x2000, the arrows 0x48/0x50/0x4B/0x4D00). // Each cycle every queued key goes, in order, to a set.key controller // that names it, then to steering ego, then to the command line. Enter // on the command line parses it: punctuation removed, lower case, the // longest dictionary entries matched word by word, ignored words (group // 0) dropped; f2 is set and f4 cleared. Parsing stops at an unknown word, // which stays in the list (any-word and rest-of-line still match it) with // its position in v9; a line of nothing but ignored words says nothing. #include "agi.h" #include #include #include "joey/core.h" #include "joey/input.h" #include "joey/present.h" #define KEY_QUEUE_SIZE 16u #define ASCII_FIRST_PRINT 0x20u #define ASCII_LAST_PRINT 0x7Eu #define ASCII_CTRL_FIRST 1u #define SCAN_SHIFT 8u #define FKEY_SCAN_FIRST 0x3Bu #define FKEY_COUNT 10u #define JOY_DEADZONE 48 #define HALF_SECOND_MS 500u #define NUMBER_MAX_DIGITS 3u #define NUMBER_MAX 255u #define DECIMAL_BASE 10u #define LETTER_COUNT 26u // Characters the parser treats as word breaks. #define PARSE_SEPARATORS " ,.?!();:[]{}" // ----- Prototypes ----- static uint16_t dequeue(void); static bool editLine(char *line, uint8_t *len, uint8_t maxLen, uint16_t key); static void enqueue(uint16_t key); static void handleKey(uint16_t key); static uint8_t joystickDirection(void); static uint8_t keyDirection(uint16_t key); // ----- Tables ----- // The scan code of each letter, for Alt+letter. static const uint8_t kLetterScan[LETTER_COUNT] = { 0x1E, 0x30, 0x2E, 0x20, 0x12, 0x21, 0x22, 0x23, 0x17, 0x24, 0x25, 0x26, 0x32, 0x31, 0x18, 0x19, 0x10, 0x13, 0x1F, 0x14, 0x16, 0x2F, 0x11, 0x2D, 0x15, 0x2C }; static const jlKeyE kFunctionKeys[FKEY_COUNT] = { KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 }; // ----- Module state ----- static uint16_t gQueue[KEY_QUEUE_SIZE]; static uint8_t gQueueHead; static uint8_t gQueueTail; static uint8_t gJoyDir; // A key this cycle that nothing took (no controller, not steering ego, // no command line to type into), left for have.key. static bool gKeyUnused; // have.key has been asked this cycle. static bool gHaveKeyAsked; // ----- Internal helpers (alphabetical) ----- static uint16_t dequeue(void) { uint16_t key; if (gQueueHead == gQueueTail) { return AGI_KEY_NONE; } key = gQueue[gQueueTail]; gQueueTail = (uint8_t)((gQueueTail + 1u) % KEY_QUEUE_SIZE); return key; } // Apply one key to a line being typed. Returns true when it was Enter. static bool editLine(char *line, uint8_t *len, uint8_t maxLen, uint16_t key) { if (key == AGI_KEY_ENTER) { return true; } if (key == AGI_KEY_BACKSPACE) { if (*len > 0u) { (*len)--; line[*len] = '\0'; } return false; } if (key >= ASCII_FIRST_PRINT && key <= ASCII_LAST_PRINT && *len < maxLen) { line[*len] = (char)key; (*len)++; line[*len] = '\0'; } return false; } static void enqueue(uint16_t key) { uint8_t next; next = (uint8_t)((gQueueHead + 1u) % KEY_QUEUE_SIZE); if (next == gQueueTail) { return; } gQueue[gQueueHead] = key; gQueueHead = next; } static void handleKey(uint16_t key) { uint8_t i; uint8_t dir; uint8_t maxLen; uint8_t oldLen; gAgi->vars[VAR_KEY] = (uint8_t)key; for (i = 0u; i < gAgi->keyMapCount; i++) { if (gAgi->keyMaps[i].key == key) { gAgi->controllers[gAgi->keyMaps[i].controller] = 1u; return; } } dir = keyDirection(key); if (dir != AGI_DIR_NONE) { // A second press of the direction ego is walking stops it. if (gAgi->playerControl) { gAgi->vars[VAR_EGO_DIR] = (gAgi->vars[VAR_EGO_DIR] == dir) ? AGI_DIR_NONE : dir; gAgi->objects[AGI_EGO].direction = gAgi->vars[VAR_EGO_DIR]; } return; } if (!gAgi->inputOn) { gKeyUnused = true; return; } maxLen = (uint8_t)(AGI_TEXT_COLS - 1u - strlen(gAgi->strings[0])); if (gAgi->vars[VAR_INPUT_MAX] != 0u && gAgi->vars[VAR_INPUT_MAX] < maxLen) { maxLen = gAgi->vars[VAR_INPUT_MAX]; } if (maxLen > AGI_INPUT_MAX) { maxLen = AGI_INPUT_MAX; } oldLen = gAgi->inputLen; if (!editLine(gAgi->inputLine, &gAgi->inputLen, maxLen, key)) { textEchoInput(oldLen); return; } if (gAgi->inputLen > 0u) { strcpy(gAgi->lastLine, gAgi->inputLine); inputParse(gAgi->inputLine); gAgi->inputLine[0] = '\0'; gAgi->inputLen = 0u; } textDrawInputLine(); } // The joystick, read as one of the eight directions. static uint8_t joystickDirection(void) { return objDirectionTo(0, 0, jlJoystickX(JOYSTICK_0), jlJoystickY(JOYSTICK_0), JOY_DEADZONE); } static uint8_t keyDirection(uint16_t key) { switch (key) { case AGI_KEY_UP: return AGI_DIR_N; case AGI_KEY_RIGHT: return AGI_DIR_E; case AGI_KEY_DOWN: return AGI_DIR_S; case AGI_KEY_LEFT: return AGI_DIR_W; default: return AGI_DIR_NONE; } } // ----- Public API (alphabetical) ----- // get.num: a number typed after a prompt on the input line. bool inputGetNumber(const char *prompt, uint8_t *outValue) { char buf[NUMBER_MAX_DIGITS + 1u]; uint8_t len; uint16_t value; uint8_t i; buf[0] = '\0'; if (!inputGetString(buf, NUMBER_MAX_DIGITS, AGI_NO_POSITION, AGI_NO_POSITION, prompt)) { return false; } value = 0u; len = (uint8_t)strlen(buf); for (i = 0u; i < len; i++) { if (buf[i] < '0' || buf[i] > '9') { break; } value = (uint16_t)(value * DECIMAL_BASE + (uint16_t)(buf[i] - '0')); } *outValue = (uint8_t)((value > NUMBER_MAX) ? NUMBER_MAX : value); return true; } // get.string (and get.num): a line typed at (row, col) after a prompt; // without a position, on the input line. Escape cancels. The typed text // is left in dst. bool inputGetString(char *dst, uint8_t maxLen, uint8_t row, uint8_t col, const char *prompt) { char line[AGI_INPUT_MAX + 1u]; char shown[AGI_INPUT_MAX + 2u]; uint8_t len; uint16_t key; uint8_t fieldCol; bool onInputLine; bool done; if (maxLen > AGI_INPUT_MAX) { maxLen = AGI_INPUT_MAX; } onInputLine = (row == AGI_NO_POSITION); if (onInputLine) { row = gAgi->inputRow; col = 0u; textClearLines(row, row, AGI_COLOR_BLACK); } fieldCol = (uint8_t)(col + strlen(prompt)); textDisplay(row, col, prompt); line[0] = '\0'; len = 0u; done = false; while (!done) { memcpy(shown, line, len); shown[len] = gAgi->cursorChar; shown[len + 1u] = '\0'; textClearRect(row, fieldCol, row, (uint8_t)(fieldCol + maxLen), AGI_COLOR_BLACK); textDisplay(row, fieldCol, shown); key = inputWaitKey(); if (key == AGI_KEY_ESCAPE) { break; } done = editLine(line, &len, maxLen, key); } if (onInputLine) { textDrawInputLine(); } if (!done) { return false; } strcpy(dst, line); return true; } // have.key: a key this cycle that nothing else took. A logic that asks // again in the same cycle is waiting for a key (KQ3's help screen spins // on have.key), so from then on a frame passes per ask and any key that // arrives counts, as Sierra's interpreter reads the keyboard there. bool inputHaveKey(void) { uint16_t key; if (gKeyUnused) { gKeyUnused = false; return true; } if (!gHaveKeyAsked) { gHaveKeyAsked = true; return false; } agiIdle(); key = dequeue(); if (key == AGI_KEY_NONE) { return false; } gAgi->vars[VAR_KEY] = (uint8_t)key; return true; } // Parse a typed line into word groups for said(). void inputParse(const char *line) { char text[AGI_INPUT_MAX + 1u]; uint8_t n; uint8_t pos; uint8_t len; uint16_t id; bool space; // Lower case, punctuation to spaces, runs of spaces to one. n = 0u; space = true; while (*line != '\0' && n < AGI_INPUT_MAX) { char c = *line++; if (c >= 'A' && c <= 'Z') { c = (char)(c - 'A' + 'a'); } if (strchr(PARSE_SEPARATORS, c) != NULL) { if (!space) { text[n++] = ' '; } space = true; continue; } text[n++] = c; space = false; } while (n > 0u && text[n - 1u] == ' ') { n--; } text[n] = '\0'; gAgi->wordCount = 0u; gAgi->vars[VAR_BAD_WORD] = 0u; if (n == 0u) { return; } pos = 0u; while (text[pos] != '\0' && gAgi->wordCount < AGI_MAX_WORDS) { id = agiWordMatch(&gAgi->game, &text[pos], &len); if (id == AGI_WORD_NONE) { // Parsing stops at an unknown word, which stays in the list // (any word and the rest of the line still match it) with // its text for %w; v9 says where it was. len = 0u; while (text[pos + len] != '\0' && text[pos + len] != ' ' && len < AGI_WORD_MAX_LEN) { gAgi->wordText[gAgi->wordCount][len] = text[pos + len]; len++; } gAgi->wordText[gAgi->wordCount][len] = '\0'; gAgi->wordIds[gAgi->wordCount] = AGI_WORD_NONE; gAgi->wordCount++; gAgi->vars[VAR_BAD_WORD] = gAgi->wordCount; break; } if (id != AGI_WORD_IGNORE) { memcpy(gAgi->wordText[gAgi->wordCount], &text[pos], len); gAgi->wordText[gAgi->wordCount][len] = '\0'; gAgi->wordIds[gAgi->wordCount] = id; gAgi->wordCount++; } pos = (uint8_t)(pos + len); if (text[pos] == ' ') { pos++; } } // A line of nothing but ignored words says nothing. if (gAgi->wordCount == 0u) { return; } gAgi->flags[FLAG_ENTERED] = 1u; gAgi->flags[FLAG_SAID_OK] = 0u; } // Gather this frame's key presses into the queue as AGI key codes. void inputPoll(void) { int ch; uint8_t i; bool alt; bool ctrl; uint8_t dir; jlInputPoll(); alt = jlKeyDown(KEY_LALT); ctrl = jlKeyDown(KEY_LCTRL); // A Ctrl or Alt chord is a command, not typing, whatever the port // delivers as a character. while ((ch = jlInputGetChar()) >= 0) { if (!alt && !ctrl) { enqueue((uint16_t)ch); } } for (i = 0u; i < (uint8_t)(KEY_Z - KEY_A + 1); i++) { if (jlKeyPressed((jlKeyE)(KEY_A + i))) { if (alt) { enqueue((uint16_t)((uint16_t)kLetterScan[i] << SCAN_SHIFT)); } else if (ctrl) { enqueue((uint16_t)(ASCII_CTRL_FIRST + i)); } } } for (i = 0u; i < FKEY_COUNT; i++) { if (jlKeyPressed(kFunctionKeys[i])) { enqueue((uint16_t)((uint16_t)(FKEY_SCAN_FIRST + i) << SCAN_SHIFT)); } } if (jlKeyPressed(KEY_UP)) { enqueue(AGI_KEY_UP); } if (jlKeyPressed(KEY_DOWN)) { enqueue(AGI_KEY_DOWN); } if (jlKeyPressed(KEY_LEFT)) { enqueue(AGI_KEY_LEFT); } if (jlKeyPressed(KEY_RIGHT)) { enqueue(AGI_KEY_RIGHT); } if (jlJoystickConnected(JOYSTICK_0)) { if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { enqueue(AGI_KEY_JOY_BUTTON_1); } if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_1)) { enqueue(AGI_KEY_JOY_BUTTON_2); } dir = joystickDirection(); if (dir != gJoyDir) { gJoyDir = dir; if (gAgi->playerControl) { gAgi->objects[AGI_EGO].direction = dir; gAgi->vars[VAR_EGO_DIR] = dir; } } } } // The start of a cycle: hand every queued key to the game. void inputProcess(void) { uint16_t key; gKeyUnused = false; gHaveKeyAsked = false; gAgi->vars[VAR_KEY] = 0u; while ((key = dequeue()) != AGI_KEY_NONE) { handleKey(key); } } void inputReset(void) { gQueueHead = 0u; gQueueTail = 0u; gKeyUnused = false; } // Block until a key, keeping the game's clock and sound going. uint16_t inputWaitKey(void) { uint16_t key; for (;;) { key = dequeue(); if (key != AGI_KEY_NONE) { return key; } agiIdle(); } } // Wait for a message window to be dismissed: Enter or Escape, or after // timeoutHalfSeconds half-seconds when that is not 0. Returns false on // Escape. bool inputWaitPrintClose(uint16_t timeoutHalfSeconds) { uint32_t start; uint16_t key; start = jlMillisElapsed(); for (;;) { key = dequeue(); if (key == AGI_KEY_ENTER) { return true; } if (key == AGI_KEY_ESCAPE) { return false; } if (timeoutHalfSeconds != 0u && jlMillisElapsed() - start >= (uint32_t)timeoutHalfSeconds * HALF_SECOND_MS) { return true; } agiIdle(); } }