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