joeyagi/agiInput.c
2026-10-06 16:59:41 -05:00

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();
}
}