// 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] = { JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5, JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10 }; // ----- Module state ----- static uint16_t gQueue[KEY_QUEUE_SIZE]; static uint8_t gQueueHead; static uint8_t gQueueTail; static uint8_t gJoyDir; // The direction key last pressed: under hold.key, letting it go stops ego. static jlKeyE gHoldLastKey; // 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) { char prompt[AGI_TEXT_COLS + 1u]; uint8_t i; uint8_t dir; uint8_t maxLen; uint8_t oldLen; // A click is the mouse's (the menus or a walk), not a key for v19. if (key == AGI_KEY_CLICK) { mouseClick(); return; } 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; } } if (key == AGI_KEY_STATIONARY) { if (gAgi->playerControl) { gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE; gAgi->objects[AGI_EGO].direction = AGI_DIR_NONE; } return; } dir = keyDirection(key); if (dir != AGI_DIR_NONE) { // A second press of the direction ego is walking stops it; any // stops a walk to a click. if (gAgi->playerControl) { if (gAgi->objects[AGI_EGO].motion == AGI_MOTION_MOUSE) { gAgi->objects[AGI_EGO].motion = AGI_MOTION_NORMAL; } 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 - textPrompt(prompt)); 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(JL_JOYSTICK_0), jlJoystickY(JL_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; uint8_t shownLen; uint8_t drawn; 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; drawn = 0u; done = false; while (!done) { // The text and the cursor in the current attribute, as Sierra's // draws them (nothing clears the field first: Mother Goose's name // is black on its white window); spaces over what a backspace // left behind. memcpy(shown, line, len); shown[len] = gAgi->cursorChar; shown[len + 1u] = '\0'; shownLen = (uint8_t)strlen(shown); while (shownLen < drawn) { shown[shownLen++] = ' '; } shown[shownLen] = '\0'; drawn = shownLen; 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, else (as Sierra's // 2.915 does) a key that arrived since the cycle's keys were handled, // taken as it is: one set.key maps to a controller counts too when it comes // in while the logics run (Demo 2's Return at its "Press any key"). 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. bool inputHaveKey(void) { uint16_t key; if (gKeyUnused) { gKeyUnused = false; return true; } if (!gHaveKeyAsked) { gHaveKeyAsked = true; inputPoll(); } else { 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]; char typed[AGI_INPUT_MAX + 1u]; uint8_t n; uint8_t pos; uint8_t len; uint16_t id; bool space; // Punctuation to spaces, runs of spaces to one; matched in lower case, // while %w gives each word as it was typed (Sierra's "Wilco"). n = 0u; space = true; while (*line != '\0' && n < AGI_INPUT_MAX) { char c = *line++; if (strchr(PARSE_SEPARATORS, c) != NULL) { if (!space) { text[n] = ' '; typed[n] = ' '; n++; } space = true; continue; } typed[n] = c; text[n] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c; n++; 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] = typed[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], &typed[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(JL_KEY_LALT); ctrl = jlKeyDown(JL_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)(JL_KEY_Z - JL_KEY_A + 1); i++) { if (jlKeyPressed((jlKeyE)(JL_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 (jlMousePressed(JL_MOUSE_BUTTON_LEFT)) { enqueue(AGI_KEY_CLICK); } if (jlKeyPressed(JL_KEY_UP)) { enqueue(AGI_KEY_UP); gHoldLastKey = JL_KEY_UP; } if (jlKeyPressed(JL_KEY_DOWN)) { enqueue(AGI_KEY_DOWN); gHoldLastKey = JL_KEY_DOWN; } if (jlKeyPressed(JL_KEY_LEFT)) { enqueue(AGI_KEY_LEFT); gHoldLastKey = JL_KEY_LEFT; } if (jlKeyPressed(JL_KEY_RIGHT)) { enqueue(AGI_KEY_RIGHT); gHoldLastKey = JL_KEY_RIGHT; } // hold.key: ego walks only while the direction key last pressed is // down (letting go of an earlier one changes nothing). if (gAgi->holdKey && gHoldLastKey != JL_KEY_NONE && jlKeyReleased(gHoldLastKey)) { enqueue(AGI_KEY_STATIONARY); gHoldLastKey = JL_KEY_NONE; } if (jlJoystickConnected(JL_JOYSTICK_0)) { if (jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_0)) { enqueue(AGI_KEY_JOY_BUTTON_1); } if (jlJoyPressed(JL_JOYSTICK_0, JL_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; gHoldLastKey = JL_KEY_NONE; } // Block until a key, keeping the game's clock and sound going. // A key if one is waiting, else one that comes in over the next frame, // else AGI_KEY_NONE (the menus watch the pointer between keys). uint16_t inputPollKey(void) { uint16_t key; key = dequeue(); if (key == AGI_KEY_NONE) { agiIdle(); key = dequeue(); } return key; } uint16_t inputWaitKey(void) { uint16_t key; do { key = inputPollKey(); } while (key == AGI_KEY_NONE); return key; } // 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(); // A click in a message window closes it as Enter does. if (key == AGI_KEY_ENTER || key == AGI_KEY_CLICK) { return true; } if (key == AGI_KEY_ESCAPE) { return false; } if (timeoutHalfSeconds != 0u && jlMillisElapsed() - start >= (uint32_t)timeoutHalfSeconds * HALF_SECOND_MS) { return true; } agiIdle(); } }