joeyagi/tests/hostPort.c
2026-10-19 15:47:28 -05:00

1144 lines
41 KiB
C

// Headless host port of JoeyLib for running the whole interpreter on the
// build machine: JoeyLib's BLANK platform with these services filled in.
//
// Time is virtual. Every jlWaitVBL is one 1/60 s frame, so a run is exactly
// reproducible: the same script gives the same frames, independent of how
// fast the host is. The display is the stage itself, resolved through each
// row's palette at every present.
//
// The input script is the one scripts/dosDrive.py feeds DOSBox, so the same
// file drives Sierra's interpreter, JoeyAGI in DOSBox and this harness:
// wait SECONDS let SECONDS of virtual time pass
// key NAME [HOLD] press key NAME (xdotool names) for HOLD seconds
// down NAME / up NAME hold / release a key
// type TEXT press each character's key in turn, typing it
// say TEXT type TEXT, press Return, give the game a second
// to answer and clear its message windows
// shot NAME write the displayed frame to OUTDIR/NAME.ppm
// clear press Return until no message window is open
// record NAME note every frame from here ...
// stop ... to here: OUTDIR/NAME.frames lists each
// frame's hash and OUTDIR/NAME-HASH.ppm holds each
// different frame once
// until [SECONDS] COND wait until COND holds (at most SECONDS, 120 by
// default; longer fails the run)
// await [SECONDS] COND as until, but running out of time is no failure:
// a goto after it can test what happened
// check COND fail the run unless COND holds now
// walk X Y steer ego with the arrow keys to (X, Y) along
// the straight line from where it is, zig-zagging
// on a diagonal; '-' leaves an axis alone. Over
// when ego gets there or the room changes; an ego
// that stops moving on the way fails the run
// walk oN steer ego to within 8 of object N (as the
// distance command measures), following it
// state NAME print the game's progress (room, score, items
// carried, flags) and hash it like a shot
// show THING... print variables (vN), flags (fN), items' rooms
// (iN), objects (oN) and ego's place (x, y), for
// following a run
// label NAME a place goto can go back (or on) to
// goto NAME [if COND] go on from 'label NAME' (when COND holds)
// A COND is terms joined by 'and': vN, iN (item N's room, 255 carried),
// x or y (ego's position) compared with == != < > <= >= to a number, or
// fN / !fN, or window / !window (a message window is open). While an until waits, a message window left open for 25
// seconds (longer than any that closes by itself) is dismissed with
// Return, and while a walk walks one left open for a second: the game's
// chatter at random moments must not stall them.
// '#' starts a comment. When the script ends the run stops.
//
// Every shot prints its frame's FNV-1a hash (the gate compares those) and,
// unless the output folder is "-", writes a PPM; a recording prints one
// hash for its whole sequence of frames, as a shot named after it.
#include "hostPort.h"
#include "port.h"
#include "surfaceInternal.h"
#include "inputInternal.h"
#include "agi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HOST_FRAME_HZ 60u
#define HOST_LINE_MAX 512
#define HOST_PATH_MAX 1024
// A file path: the output folder, a name from the script and a suffix.
#define HOST_FILE_MAX (HOST_PATH_MAX + HOST_LINE_MAX + 64)
#define HOST_KEY_HOLD_FRAMES 7u // 0.12 s, dosDrive.py's default press
#define HOST_KEY_GAP_FRAMES 5u // 0.08 s, dosDrive.py's gap after a press
#define HOST_CHANNEL_MAX 255u
#define HOST_NIBBLE_SCALE 17u // $0RGB nibble -> 8-bit channel
#define HOST_PIXELS_PER_BYTE 2u
#define HOST_CLEAR_GAP_FRAMES 36u // 0.6 s, dosDrive.py's pause between Returns
#define HOST_CLEAR_TRIES 8u // dosDrive.py's limit on Returns per clear
#define HOST_SAY_SETTLE_FRAMES 60u // 1 s, dosDrive.py's wait for an answer to a 'say'
#define HOST_NO_FILES "-"
#define FNV_OFFSET_BASIS 2166136261u
#define FNV_PRIME 16777619u
#define HASH_BYTES 4u
#define BYTE_BITS 8u
#define BYTE_MASK 0xFFu
#define RECORD_DISTINCT_MAX 64u
#define HOST_EXIT_FAILED 3
#define HOST_COND_TERMS_MAX 8u
#define HOST_UNTIL_SECONDS 120.0 // an until's limit unless it names one
#define HOST_WALK_ANY INT16_MIN // a walk axis left alone ('-')
#define HOST_WALK_STUCK_FRAMES 600u // 10 s without getting nearer: ego is blocked
#define HOST_WALK_SIDESTEP_FRAMES 30u // a first step aside round something in the way
#define HOST_WALK_BUMP_FRAMES 24u // a pressed way that gets nowhere this long is blocked
// A walk's presses are short, so it looks again before ego (a step a
// cycle, 6 frames at normal speed) can pass the place it is going to.
#define HOST_WALK_HOLD_FRAMES 2u
#define HOST_WALK_GAP_FRAMES 1u
#define HOST_WALK_SETTLE_FRAMES 15u // a press has surely been seen by then
#define HOST_WALK_MAX_FRAMES 7200u // 2 minutes for one walk at most
// A window left open this long while an until waits (longer than any
// window that closes by itself) or a walk walks is dismissed.
#define HOST_UNTIL_PATIENCE_FRAMES 1500u
#define HOST_WALK_PATIENCE_FRAMES 60u
#define HOST_FLAG_BITS 8u
#define HOST_GOTO_MAX 1000u // more jumps than this is a script stuck in a loop
#define HOST_WALK_NO_OBJECT 0xFFu // a walk to a place, not to an object
#define HOST_WALK_NEAR 8 // how near (as distance measures) a walk to an object goes
#define HOST_WALK_BAND 1 // pixels a walk may stray from its line
#define HOST_FLAG_TOP_BIT 0x80u // flag 0 of a byte
typedef struct {
const char *name;
jlKeyE key;
uint8_t ch;
} HostKeyNameT;
// What a condition term reads.
typedef enum {
HOST_TERM_VAR = 0,
HOST_TERM_FLAG = 1,
HOST_TERM_ITEM = 2,
HOST_TERM_EGO_X = 3,
HOST_TERM_EGO_Y = 4,
HOST_TERM_WINDOW = 5
} HostTermKindE;
typedef enum {
HOST_OP_EQ = 0,
HOST_OP_NE = 1,
HOST_OP_LT = 2,
HOST_OP_GT = 3,
HOST_OP_LE = 4,
HOST_OP_GE = 5
} HostOpE;
// One term of a condition: kind[index] op value.
typedef struct {
HostTermKindE kind;
uint8_t index;
HostOpE op;
int16_t value;
} HostTermT;
// Where a 'say' is: typing, Return pressed, the answer cleared away.
typedef enum {
HOST_SAY_NONE = 0,
HOST_SAY_TYPING = 1,
HOST_SAY_SETTLE = 2,
HOST_SAY_CLEAR = 3
} HostSayE;
// What the script is waiting on besides time.
typedef enum {
HOST_WAIT_NONE = 0,
HOST_WAIT_UNTIL = 1,
HOST_WAIT_WALK = 2
} HostWaitE;
// ----- Prototypes -----
static bool hostClearStep(void);
static bool hostCondHolds(void);
static bool hostCondParse(char *text);
static void hostDump(const char *name);
static void hostGoto(const char *label);
static jlKeyE hostEgoKey(uint8_t dir);
static void hostFail(const char *why);
static uint32_t hostFrameHash(void);
static uint32_t hostHashBytes(uint32_t hash, const uint8_t *p, size_t n);
static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh);
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames);
static void hostRecordFrame(void);
static void hostShow(char *things);
static void hostState(const char *name);
static void hostStep(void);
static bool hostTermHolds(const HostTermT *t);
static bool hostWaitStep(void);
static bool hostWalkStep(void);
static void hostWritePpm(const char *path);
// ----- Module state -----
static const HostKeyNameT kKeyNames[] = {
{ "Return", JL_KEY_RETURN, JL_CHAR_RETURN },
{ "Escape", JL_KEY_ESCAPE, JL_CHAR_ESCAPE },
{ "Tab", JL_KEY_TAB, JL_CHAR_TAB },
{ "BackSpace", JL_KEY_BACKSPACE, JL_CHAR_BACKSPACE },
{ "space", JL_KEY_SPACE, ' ' },
{ "Up", JL_KEY_UP, 0u },
{ "Down", JL_KEY_DOWN, 0u },
{ "Left", JL_KEY_LEFT, 0u },
{ "Right", JL_KEY_RIGHT, 0u },
{ "F1", JL_KEY_F1, 0u },
{ "F2", JL_KEY_F2, 0u },
{ "F3", JL_KEY_F3, 0u },
{ "F4", JL_KEY_F4, 0u },
{ "F5", JL_KEY_F5, 0u },
{ "F6", JL_KEY_F6, 0u },
{ "F7", JL_KEY_F7, 0u },
{ "F8", JL_KEY_F8, 0u },
{ "F9", JL_KEY_F9, 0u },
{ "F10", JL_KEY_F10, 0u },
{ "ctrl", JL_KEY_LCTRL, 0u },
{ "alt", JL_KEY_LALT, 0u },
{ "shift", JL_KEY_LSHIFT, 0u },
{ "period", JL_KEY_NONE, '.' },
{ "comma", JL_KEY_NONE, ',' },
{ "apostrophe", JL_KEY_NONE, '\'' },
{ "minus", JL_KEY_NONE, '-' },
{ "question", JL_KEY_NONE, '?' },
{ "exclam", JL_KEY_NONE, '!' }
};
// The comparison operators of a condition, in HostOpE order.
static const char *const kOps[] = { "==", "!=", "<", ">", "<=", ">=" };
static FILE *gScript;
static char gOutDir[HOST_PATH_MAX];
static uint32_t gFrame;
static uint32_t gResumeFrame;
static uint32_t gHoldUntil[JL_KEY_COUNT];
static bool gHeld[JL_KEY_COUNT];
static uint8_t gDisplay[JL_SURFACE_HEIGHT][JL_SURFACE_WIDTH][3];
// Characters still to type for the current 'type' line, one per press.
static char gTypeBuf[HOST_LINE_MAX];
static long gLinePos;
static uint16_t gTypePos;
static HostSayE gSay;
static uint8_t gClearTries;
// A recording: its name, its frame list, the hash of its frame sequence
// and the frames written so far.
static char gRecordName[HOST_LINE_MAX];
static FILE *gRecordLog;
static bool gRecording;
static uint32_t gRecordHash;
static uint32_t gRecordFrames;
static uint32_t gRecordSeen[RECORD_DISTINCT_MAX];
static uint8_t gRecordSeenCount;
// The script line being run, for failure messages.
static char gOpLine[HOST_LINE_MAX];
// An until's condition or a walk's target, and how long it may take.
static HostWaitE gWait;
static HostTermT gTerms[HOST_COND_TERMS_MAX];
static uint8_t gTermCount;
static uint32_t gWaitDeadline;
// An until fails the run when its time runs out; an await does not.
static bool gWaitMayFail;
static int16_t gWalkX;
static int16_t gWalkY;
static int16_t gWalkFromX;
static int16_t gWalkFromY;
static uint8_t gWalkObject;
static uint8_t gWalkRoom;
static int16_t gWalkLastX;
static int16_t gWalkLastY;
static int16_t gWalkBest;
static uint32_t gWalkMovedFrame;
static uint32_t gWalkTriedFrame;
static bool gWalkAxisY;
// A step aside round something in the way: which way, until when, and
// how many there have been since ego last got nearer.
static uint8_t gWalkSideDir;
static uint32_t gWalkSideUntil;
static uint8_t gWalkSideTries;
// The direction ego should take up after the walk's last press, and when.
static uint8_t gWalkExpectDir;
static uint32_t gWalkPressFrame;
// A message window seen open during a wait, and since when.
static bool gWindowSeen;
static uint32_t gWindowSince;
// AGI_HOST_TRACE in the environment: log every key a walk presses.
static bool gTrace;
static uint16_t gGotoCount;
// ----- Internal helpers (alphabetical) -----
// Dismiss message windows: Return while one is open, a few times at most
// (a window that wants another key stays). True when none is left.
static bool hostClearStep(void) {
if (gAgi != NULL && gAgi->windowOpen && gClearTries < HOST_CLEAR_TRIES) {
gClearTries++;
hostPress(JL_KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
gResumeFrame = gFrame + HOST_CLEAR_GAP_FRAMES;
return false;
}
gClearTries = 0u;
return true;
}
// Does every term of the condition (gTerms) hold?
static bool hostCondHolds(void) {
uint8_t i;
for (i = 0u; i < gTermCount; i++) {
if (!hostTermHolds(&gTerms[i])) {
return false;
}
}
return true;
}
// Parse a condition, terms joined by 'and', into gTerms. False when it
// does not read.
static bool hostCondParse(char *text) {
char *tok;
char *opTok;
char *valueTok;
HostTermT *t;
uint8_t op;
bool negated;
gTermCount = 0u;
tok = strtok(text, " \t");
while (tok != NULL) {
if (gTermCount >= HOST_COND_TERMS_MAX) {
return false;
}
t = &gTerms[gTermCount++];
negated = (tok[0] == '!');
if (strcmp(tok + (negated ? 1 : 0), "window") == 0) {
t->kind = HOST_TERM_WINDOW;
t->index = 0u;
t->op = negated ? HOST_OP_EQ : HOST_OP_NE;
t->value = 0;
} else if (tok[negated ? 1 : 0] == 'f') {
t->kind = HOST_TERM_FLAG;
t->index = (uint8_t)atoi(tok + (negated ? 2 : 1));
t->op = negated ? HOST_OP_EQ : HOST_OP_NE;
t->value = 0;
} else {
if (tok[0] == 'v') {
t->kind = HOST_TERM_VAR;
} else if (tok[0] == 'i') {
t->kind = HOST_TERM_ITEM;
} else if (strcmp(tok, "x") == 0) {
t->kind = HOST_TERM_EGO_X;
} else if (strcmp(tok, "y") == 0) {
t->kind = HOST_TERM_EGO_Y;
} else {
return false;
}
t->index = (uint8_t)atoi(tok + 1);
opTok = strtok(NULL, " \t");
valueTok = strtok(NULL, " \t");
if (opTok == NULL || valueTok == NULL) {
return false;
}
for (op = 0u; op < sizeof(kOps) / sizeof(kOps[0]) && strcmp(kOps[op], opTok) != 0; op++) {
}
if (op == sizeof(kOps) / sizeof(kOps[0])) {
return false;
}
t->op = (HostOpE)op;
t->value = (int16_t)atoi(valueTok);
}
tok = strtok(NULL, " \t");
if (tok == NULL) {
break;
}
if (strcmp(tok, "and") != 0) {
return false;
}
tok = strtok(NULL, " \t");
if (tok == NULL) {
return false;
}
}
return gTermCount > 0u;
}
static void hostDump(const char *name) {
char path[HOST_FILE_MAX];
printf("[%6.1f] shot %s %08x\n", (double)gFrame / (double)HOST_FRAME_HZ, name, (unsigned)hostFrameHash());
if (strcmp(gOutDir, HOST_NO_FILES) == 0) {
return;
}
snprintf(path, sizeof(path), "%s/%s.ppm", gOutDir, name);
hostWritePpm(path);
}
// Go on from the line after 'label NAME'.
static void hostGoto(const char *label) {
char line[HOST_LINE_MAX];
char *p;
if (++gGotoCount > HOST_GOTO_MAX) {
hostFail("too many jumps: the script is going round in a loop");
}
rewind(gScript);
while (fgets(line, sizeof(line), gScript) != NULL) {
line[strcspn(line, "#\r\n")] = '\0';
p = line + strspn(line, " \t");
if (strncmp(p, "label", 5) != 0 || (p[5] != ' ' && p[5] != '\t')) {
continue;
}
p += 5;
p += strspn(p, " \t");
p[strcspn(p, " \t")] = '\0';
if (strcmp(p, label) == 0) {
return;
}
}
hostFail("no such label");
}
// The arrow key that turns ego to dir (north for a diagonal, which the
// arrows cannot name: pressing it again then stops ego).
static jlKeyE hostEgoKey(uint8_t dir) {
switch (dir) {
case AGI_DIR_E:
return JL_KEY_RIGHT;
case AGI_DIR_S:
return JL_KEY_DOWN;
case AGI_DIR_W:
return JL_KEY_LEFT;
default:
return JL_KEY_UP;
}
}
// The script cannot go on: say why and end the run without 'script done'.
static void hostFail(const char *why) {
printf("[%6.1f] FAIL '%s': %s (room %u, ego at %d,%d)\n", (double)gFrame / (double)HOST_FRAME_HZ, gOpLine, why,
(unsigned)gAgi->vars[VAR_ROOM], (int)gAgi->objects[AGI_EGO].x, (int)gAgi->objects[AGI_EGO].y);
exit(HOST_EXIT_FAILED);
}
static uint32_t hostFrameHash(void) {
return hostHashBytes(FNV_OFFSET_BASIS, &gDisplay[0][0][0], sizeof(gDisplay));
}
// FNV-1a: hash carried on over n more bytes.
static uint32_t hostHashBytes(uint32_t hash, const uint8_t *p, size_t n) {
size_t i;
for (i = 0; i < n; i++) {
hash = (hash ^ p[i]) * FNV_PRIME;
}
return hash;
}
// Map an xdotool key name (or a single character) to a JoeyLib key and
// the character it types.
static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh) {
size_t i;
char c;
for (i = 0; i < sizeof(kKeyNames) / sizeof(kKeyNames[0]); i++) {
if (strcmp(kKeyNames[i].name, name) == 0) {
*outKey = kKeyNames[i].key;
*outCh = kKeyNames[i].ch;
return true;
}
}
if (name[0] == '\0' || name[1] != '\0') {
return false;
}
c = name[0];
if (c >= 'a' && c <= 'z') {
*outKey = (jlKeyE)(JL_KEY_A + (c - 'a'));
*outCh = (uint8_t)c;
return true;
}
if (c >= 'A' && c <= 'Z') {
*outKey = (jlKeyE)(JL_KEY_A + (c - 'A'));
*outCh = (uint8_t)c;
return true;
}
if (c >= '0' && c <= '9') {
*outKey = (jlKeyE)(JL_KEY_0 + (c - '0'));
*outCh = (uint8_t)c;
return true;
}
*outKey = JL_KEY_NONE;
*outCh = (uint8_t)c;
return true;
}
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames) {
if (key != JL_KEY_NONE) {
gHoldUntil[key] = gFrame + holdFrames;
}
// Like the real keyboards, a Ctrl or Alt chord types no character.
if (ch != 0u && !gHeld[JL_KEY_LALT] && !gHeld[JL_KEY_LCTRL]) {
jlInputCharPush(ch);
}
gResumeFrame = gFrame + holdFrames + gapFrames;
}
// One frame of a recording: into the sequence hash and the frame list,
// and written out the first time it is seen.
static void hostRecordFrame(void) {
char path[HOST_FILE_MAX];
uint8_t bytes[HASH_BYTES];
uint32_t hash;
uint8_t i;
hash = hostFrameHash();
for (i = 0u; i < HASH_BYTES; i++) {
bytes[i] = (uint8_t)((hash >> (i * BYTE_BITS)) & BYTE_MASK);
}
gRecordHash = hostHashBytes(gRecordHash, bytes, HASH_BYTES);
gRecordFrames++;
if (gRecordLog == NULL) {
return;
}
fprintf(gRecordLog, "%u %08x\n", (unsigned)gRecordFrames, (unsigned)hash);
for (i = 0u; i < gRecordSeenCount; i++) {
if (gRecordSeen[i] == hash) {
return;
}
}
if (gRecordSeenCount >= RECORD_DISTINCT_MAX) {
return;
}
gRecordSeen[gRecordSeenCount++] = hash;
snprintf(path, sizeof(path), "%s/%s-%08x.ppm", gOutDir, gRecordName, (unsigned)hash);
hostWritePpm(path);
}
// Print what the script names: vN, fN, iN, oN, x, y.
static void hostShow(char *things) {
char *tok;
printf("[%6.1f] show", (double)gFrame / (double)HOST_FRAME_HZ);
for (tok = strtok(things, " \t"); tok != NULL; tok = strtok(NULL, " \t")) {
switch (tok[0]) {
case 'v':
printf(" %s=%u", tok, (unsigned)gAgi->vars[(uint8_t)atoi(tok + 1)]);
break;
case 'f':
printf(" %s=%u", tok, (unsigned)gAgi->flags[(uint8_t)atoi(tok + 1)]);
break;
case 'i':
printf(" %s=%u", tok, (unsigned)gAgi->itemRoom[(uint8_t)atoi(tok + 1)]);
break;
case 'o': {
const AgiObjectT *o = &gAgi->objects[(uint8_t)atoi(tok + 1) % AGI_MAX_OBJECTS];
printf(" %s=%d,%d dir %u motion %u flags %04x", tok, (int)o->x, (int)o->y, (unsigned)o->direction, (unsigned)o->motion,
(unsigned)o->flags);
break;
}
case 'x':
printf(" x=%d", (int)gAgi->objects[AGI_EGO].x);
break;
case 'y':
printf(" y=%d", (int)gAgi->objects[AGI_EGO].y);
break;
default:
printf(" %s=?", tok);
break;
}
}
printf("\n");
}
// The game's progress: the room, the score, the items carried, every
// item's room and the flags (a bit each, flag 0 the top bit of the first
// byte), printed and hashed like a shot; where ego is, printed only.
static void hostState(const char *name) {
uint8_t bits[AGI_NUM_FLAGS / HOST_FLAG_BITS];
uint32_t hash;
uint16_t i;
double now;
memset(bits, 0, sizeof(bits));
for (i = 0u; i < AGI_NUM_FLAGS; i++) {
if (gAgi->flags[i] != 0u) {
bits[i / HOST_FLAG_BITS] = (uint8_t)(bits[i / HOST_FLAG_BITS] | (HOST_FLAG_TOP_BIT >> (i % HOST_FLAG_BITS)));
}
}
hash = hostHashBytes(FNV_OFFSET_BASIS, &gAgi->vars[VAR_ROOM], 1u);
hash = hostHashBytes(hash, &gAgi->vars[VAR_SCORE], 1u);
hash = hostHashBytes(hash, gAgi->itemRoom, gAgi->game.itemCount);
hash = hostHashBytes(hash, bits, sizeof(bits));
now = (double)gFrame / (double)HOST_FRAME_HZ;
printf("[%6.1f] state %s room=%u score=%u ego=%d,%d carried=", now, name, (unsigned)gAgi->vars[VAR_ROOM], (unsigned)gAgi->vars[VAR_SCORE],
(int)gAgi->objects[AGI_EGO].x, (int)gAgi->objects[AGI_EGO].y);
for (i = 0u; i < gAgi->game.itemCount; i++) {
if (gAgi->itemRoom[i] == AGI_ITEM_CARRIED) {
printf("%u,", (unsigned)i);
}
}
printf(" items=");
for (i = 0u; i < gAgi->game.itemCount; i++) {
printf("%02x", (unsigned)gAgi->itemRoom[i]);
}
printf(" flags=");
for (i = 0u; i < sizeof(bits); i++) {
printf("%02x", (unsigned)bits[i]);
}
printf("\n[%6.1f] shot %s %08x\n", now, name, (unsigned)hash);
}
// Run script lines until one asks for time to pass.
static void hostStep(void) {
char line[HOST_LINE_MAX];
char *op;
char *arg;
jlKeyE key;
uint8_t ch;
double secs;
while (gScript != NULL && gFrame >= gResumeFrame) {
if (gWait != HOST_WAIT_NONE) {
if (gAgi == NULL || !hostWaitStep()) {
return;
}
gWait = HOST_WAIT_NONE;
continue;
}
if (gTypeBuf[gTypePos] != '\0') {
char one[2];
one[0] = gTypeBuf[gTypePos];
one[1] = '\0';
gTypePos++;
if (one[0] == ' ') {
hostPress(JL_KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
} else if (hostKeyLookup(one, &key, &ch)) {
hostPress(key, ch, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
}
continue;
}
if (gSay == HOST_SAY_TYPING) {
hostPress(JL_KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
gSay = HOST_SAY_SETTLE;
continue;
}
if (gSay == HOST_SAY_SETTLE) {
gResumeFrame = gFrame + HOST_SAY_SETTLE_FRAMES;
gSay = HOST_SAY_CLEAR;
continue;
}
if (gSay == HOST_SAY_CLEAR) {
if (hostClearStep()) {
gSay = HOST_SAY_NONE;
}
continue;
}
gLinePos = ftell(gScript);
if (fgets(line, sizeof(line), gScript) == NULL) {
printf("[%6.1f] script done\n", (double)gFrame / (double)HOST_FRAME_HZ);
exit(0);
}
line[strcspn(line, "\r\n")] = '\0';
strcpy(gOpLine, line);
op = line;
while (*op == ' ' || *op == '\t') {
op++;
}
if (strncmp(op, "type ", 5) == 0 || strncmp(op, "say ", 4) == 0) {
printf("[%6.1f] > %s\n", (double)gFrame / (double)HOST_FRAME_HZ, op);
gSay = (op[0] == 's') ? HOST_SAY_TYPING : HOST_SAY_NONE;
strncpy(gTypeBuf, op + ((op[0] == 's') ? 4 : 5), sizeof(gTypeBuf) - 1u);
gTypeBuf[sizeof(gTypeBuf) - 1u] = '\0';
gTypePos = 0u;
continue;
}
op[strcspn(op, "#")] = '\0';
arg = op + strcspn(op, " \t");
if (*arg != '\0') {
*arg = '\0';
arg++;
while (*arg == ' ' || *arg == '\t') {
arg++;
}
}
if (*op == '\0') {
continue;
}
printf("[%6.1f] > %s\n", (double)gFrame / (double)HOST_FRAME_HZ, gOpLine + strspn(gOpLine, " \t"));
if (strcmp(op, "wait") == 0) {
secs = atof(arg);
gResumeFrame = gFrame + (uint32_t)(secs * HOST_FRAME_HZ + 0.5);
} else if (strcmp(op, "key") == 0) {
char *holdArg;
double hold;
holdArg = arg + strcspn(arg, " \t");
hold = 0.0;
if (*holdArg != '\0') {
*holdArg = '\0';
hold = atof(holdArg + 1);
}
if (!hostKeyLookup(arg, &key, &ch)) {
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
exit(2);
}
hostPress(key, ch, (hold > 0.0) ? (uint32_t)(hold * HOST_FRAME_HZ + 0.5) : HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
} else if (strcmp(op, "down") == 0 || strcmp(op, "up") == 0) {
if (!hostKeyLookup(arg, &key, &ch) || key == JL_KEY_NONE) {
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
exit(2);
}
gHeld[key] = (op[0] == 'd');
if (gHeld[key] && ch != 0u) {
jlInputCharPush(ch);
}
} else if (strcmp(op, "shot") == 0) {
hostDump(arg);
} else if (strcmp(op, "record") == 0) {
char path[HOST_FILE_MAX];
strncpy(gRecordName, arg, sizeof(gRecordName) - 1u);
gRecording = true;
gRecordHash = FNV_OFFSET_BASIS;
gRecordFrames = 0u;
gRecordSeenCount = 0u;
if (strcmp(gOutDir, HOST_NO_FILES) != 0) {
snprintf(path, sizeof(path), "%s/%s.frames", gOutDir, gRecordName);
gRecordLog = fopen(path, "w");
}
} else if (strcmp(op, "stop") == 0) {
gRecording = false;
if (gRecordLog != NULL) {
fclose(gRecordLog);
gRecordLog = NULL;
}
printf("[%6.1f] shot %s %08x\n", (double)gFrame / (double)HOST_FRAME_HZ, gRecordName, (unsigned)gRecordHash);
} else if (strcmp(op, "until") == 0 || strcmp(op, "await") == 0 || strcmp(op, "check") == 0) {
char *cond;
double limit;
cond = arg;
limit = HOST_UNTIL_SECONDS;
if (op[0] != 'c' && *arg >= '0' && *arg <= '9') {
limit = strtod(arg, &cond);
}
if (!hostCondParse(cond)) {
fprintf(stderr, "agiHost: bad condition in '%s'\n", gOpLine);
exit(2);
}
if (op[0] == 'c') {
if (!hostCondHolds()) {
hostFail("it does not hold");
}
} else {
gWait = HOST_WAIT_UNTIL;
gWaitDeadline = gFrame + (uint32_t)(limit * HOST_FRAME_HZ + 0.5);
gWaitMayFail = (op[0] == 'u');
gWindowSeen = false;
}
} else if (strcmp(op, "walk") == 0) {
char *xTok;
char *yTok;
xTok = strtok(arg, " \t");
yTok = strtok(NULL, " \t");
gWalkObject = HOST_WALK_NO_OBJECT;
if (xTok != NULL && xTok[0] == 'o') {
gWalkObject = (uint8_t)atoi(xTok + 1);
} else if (xTok == NULL || yTok == NULL) {
fprintf(stderr, "agiHost: walk wants X Y or oN in '%s'\n", gOpLine);
exit(2);
} else {
gWalkX = (strcmp(xTok, "-") == 0) ? HOST_WALK_ANY : (int16_t)atoi(xTok);
gWalkY = (strcmp(yTok, "-") == 0) ? HOST_WALK_ANY : (int16_t)atoi(yTok);
}
gWalkFromX = gAgi->objects[AGI_EGO].x;
gWalkFromY = gAgi->objects[AGI_EGO].y;
gWalkRoom = gAgi->vars[VAR_ROOM];
gWalkLastX = gAgi->objects[AGI_EGO].x;
gWalkLastY = gAgi->objects[AGI_EGO].y;
gWalkMovedFrame = gFrame;
gWalkBest = INT16_MAX;
gWalkTriedFrame = gFrame;
gWalkExpectDir = gAgi->vars[VAR_EGO_DIR];
gWalkSideUntil = gFrame;
gWalkSideTries = 0u;
gWalkPressFrame = gFrame;
gWaitDeadline = gFrame + HOST_WALK_MAX_FRAMES;
gWindowSeen = false;
gWait = HOST_WAIT_WALK;
} else if (strcmp(op, "state") == 0) {
hostState(arg);
} else if (strcmp(op, "show") == 0) {
hostShow(arg);
} else if (strcmp(op, "label") == 0) {
// Only goto reads it.
} else if (strcmp(op, "goto") == 0) {
char *name;
char *cond;
name = strtok(arg, " \t");
cond = strtok(NULL, "");
if (name == NULL) {
fprintf(stderr, "agiHost: goto wants a label in '%s'\n", gOpLine);
exit(2);
}
if (cond != NULL) {
cond += strspn(cond, " \t");
if (strncmp(cond, "if", 2) != 0 || !hostCondParse(cond + 2)) {
fprintf(stderr, "agiHost: bad condition in '%s'\n", gOpLine);
exit(2);
}
}
if (cond == NULL || hostCondHolds()) {
hostGoto(name);
}
} else if (strcmp(op, "clear") == 0) {
if (!hostClearStep()) {
fseek(gScript, gLinePos, SEEK_SET);
}
} else {
fprintf(stderr, "agiHost: bad script op '%s'\n", op);
exit(2);
}
}
}
static bool hostTermHolds(const HostTermT *t) {
int16_t have;
switch (t->kind) {
case HOST_TERM_VAR:
have = gAgi->vars[t->index];
break;
case HOST_TERM_FLAG:
have = gAgi->flags[t->index];
break;
case HOST_TERM_ITEM:
have = gAgi->itemRoom[t->index];
break;
case HOST_TERM_EGO_X:
have = gAgi->objects[AGI_EGO].x;
break;
case HOST_TERM_EGO_Y:
have = gAgi->objects[AGI_EGO].y;
break;
default:
have = gAgi->windowOpen ? 1 : 0;
break;
}
switch (t->op) {
case HOST_OP_EQ:
return have == t->value;
case HOST_OP_NE:
return have != t->value;
case HOST_OP_LT:
return have < t->value;
case HOST_OP_GT:
return have > t->value;
case HOST_OP_LE:
return have <= t->value;
default:
return have >= t->value;
}
}
// One frame of an until or a walk: true when it is over. A message
// window left open meanwhile is dismissed; the time limit runs on.
static bool hostWaitStep(void) {
if (gWait == HOST_WAIT_UNTIL && hostCondHolds()) {
return true;
}
if (gFrame >= gWaitDeadline) {
if (gWait == HOST_WAIT_UNTIL && !gWaitMayFail) {
return true;
}
hostFail((gWait == HOST_WAIT_UNTIL) ? "it never held" : "ego never got there");
}
if (gAgi->windowOpen) {
gWalkMovedFrame = gFrame;
if (!gWindowSeen) {
gWindowSeen = true;
gWindowSince = gFrame;
} else if (gFrame - gWindowSince >= ((gWait == HOST_WAIT_UNTIL) ? HOST_UNTIL_PATIENCE_FRAMES : HOST_WALK_PATIENCE_FRAMES)) {
char path[HOST_FILE_MAX];
printf("[%6.1f] dismissed a window (dismissed-%u.ppm)\n", (double)gFrame / (double)HOST_FRAME_HZ, (unsigned)gFrame);
if (strcmp(gOutDir, HOST_NO_FILES) != 0) {
snprintf(path, sizeof(path), "%s/dismissed-%u.ppm", gOutDir, (unsigned)gFrame);
hostWritePpm(path);
}
gWindowSeen = false;
hostPress(JL_KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
}
return false;
}
gWindowSeen = false;
return (gWait == HOST_WAIT_WALK) && hostWalkStep();
}
// Steer ego one frame toward the walk's target with the arrow keys,
// pressing ego's own arrow again to stop it there. A walk to a place
// keeps to the straight line from where it started, within
// HOST_WALK_BAND pixels: it stays on one axis until it strays that far,
// then takes the other (stairs on a diagonal); an axis that has bumped
// into something gives way to the other while there is any of that way
// left to go (ego feels its way along a narrow diagonal), and with one
// way left and that blocked, ego steps aside and tries again. A walk to
// an object heads for it until it is near. Over when ego is there and
// stopped, or the room changed.
static bool hostWalkStep(void) {
const AgiObjectT *ego;
int16_t slack;
int16_t dx;
int16_t dy;
int16_t left;
int32_t err;
int32_t runX;
int32_t runY;
uint8_t wantX;
uint8_t wantY;
uint8_t want;
uint8_t dir;
bool bumped;
if (gAgi->vars[VAR_ROOM] != gWalkRoom) {
return true;
}
ego = &gAgi->objects[AGI_EGO];
if (gWalkObject != HOST_WALK_NO_OBJECT) {
const AgiObjectT *o = &gAgi->objects[gWalkObject];
// Middle to middle, as the distance command measures: near
// enough is over.
gWalkX = (int16_t)(o->x + o->width / 2u - ego->width / 2u);
gWalkY = o->y;
if (abs(gWalkX - ego->x) + abs(gWalkY - ego->y) <= HOST_WALK_NEAR) {
gWalkX = ego->x;
gWalkY = ego->y;
}
}
// Progress is getting nearer (a logic that pushes ego back, or an
// overshoot, is no progress); following an object, any step is.
left = (int16_t)(((gWalkX == HOST_WALK_ANY) ? 0 : abs(gWalkX - ego->x)) + ((gWalkY == HOST_WALK_ANY) ? 0 : abs(gWalkY - ego->y)));
if (left < gWalkBest || (gWalkObject != HOST_WALK_NO_OBJECT && (ego->x != gWalkLastX || ego->y != gWalkLastY))) {
gWalkBest = left;
gWalkMovedFrame = gFrame;
gWalkSideTries = 0u;
}
gWalkLastX = ego->x;
gWalkLastY = ego->y;
// A step size of n can always land within n / 2.
slack = (int16_t)(ego->stepSize / 2u);
dx = (gWalkX == HOST_WALK_ANY || abs(gWalkX - ego->x) <= slack) ? 0 : (int16_t)(gWalkX - ego->x);
dy = (gWalkY == HOST_WALK_ANY || abs(gWalkY - ego->y) <= slack) ? 0 : (int16_t)(gWalkY - ego->y);
wantX = (dx > 0) ? AGI_DIR_E : AGI_DIR_W;
wantY = (dy > 0) ? AGI_DIR_S : AGI_DIR_N;
dir = gAgi->vars[VAR_EGO_DIR];
// A press takes effect at the next cycle: until ego's direction shows
// it (or a cycle or two passed), another press would undo it.
if (dir != gWalkExpectDir && gFrame - gWalkPressFrame < HOST_WALK_SETTLE_FRAMES) {
return false;
}
bumped = gFrame - gWalkMovedFrame >= HOST_WALK_BUMP_FRAMES && gFrame - gWalkTriedFrame >= HOST_WALK_BUMP_FRAMES;
if (dx == 0 && dy == 0) {
if (dir == AGI_DIR_NONE) {
return true;
}
gWalkExpectDir = AGI_DIR_NONE;
gWalkPressFrame = gFrame;
hostPress(hostEgoKey(dir), 0u, HOST_WALK_HOLD_FRAMES, HOST_WALK_GAP_FRAMES);
return false;
}
if (gFrame - gWalkMovedFrame >= HOST_WALK_STUCK_FRAMES) {
hostFail("ego is blocked");
}
if (bumped && (dx == 0 || dy == 0) && gFrame >= gWalkSideUntil) {
// Something stands in the only way left to go: step aside, one
// side then the other, further each time round, and try again.
gWalkSideTries++;
gWalkSideUntil = gFrame + HOST_WALK_SIDESTEP_FRAMES * ((gWalkSideTries + 1u) / 2u);
if (dx == 0) {
gWalkSideDir = (gWalkSideTries & 1u) ? AGI_DIR_E : AGI_DIR_W;
} else {
gWalkSideDir = (gWalkSideTries & 1u) ? AGI_DIR_S : AGI_DIR_N;
}
}
if (gFrame < gWalkSideUntil) {
want = gWalkSideDir;
} else if (dy == 0) {
want = wantX;
} else if (dx == 0) {
want = wantY;
} else if (bumped) {
// Whichever way was tried last got nowhere: the other one.
want = (gWalkAxisY) ? wantX : wantY;
} else if (gWalkObject != HOST_WALK_NO_OBJECT) {
// The object moves, so there is no line: keep on while ego goes
// either right way, else take the longer one.
want = (dir == wantX || dir == wantY) ? dir : ((abs(dx) >= abs(dy)) ? wantX : wantY);
} else {
// How far ego is off the line, times the line's length: keep
// going while within the band, else take the axis that closes it.
runX = (int32_t)gWalkX - gWalkFromX;
runY = (int32_t)gWalkY - gWalkFromY;
err = ((int32_t)ego->x - gWalkFromX) * runY - ((int32_t)ego->y - gWalkFromY) * runX;
if ((dir == wantX || dir == wantY) && err * err <= (int32_t)HOST_WALK_BAND * HOST_WALK_BAND * (runX * runX + runY * runY)) {
want = dir;
} else if (err == 0) {
want = (abs(dx) >= abs(dy)) ? wantX : wantY;
} else {
// A step along x changes err by runY per pixel (sign of dx).
want = (((err > 0) == ((runY > 0) == (dx > 0))) && runY != 0) ? wantY : wantX;
}
}
if (dir != want || bumped) {
if (gTrace) {
printf("[%6.1f] walk: ego at %d,%d dir %u, pressing for %u%s\n", (double)gFrame / (double)HOST_FRAME_HZ, (int)ego->x, (int)ego->y,
(unsigned)dir, (unsigned)want, bumped ? " (bumped)" : "");
}
// Pressing the same way again (a logic stopped ego) is still the
// same try: only a change of axis starts a new one.
if (gWalkAxisY != (want == wantY && dy != 0) || bumped) {
gWalkAxisY = (want == wantY) && dy != 0;
gWalkTriedFrame = gFrame;
}
gWalkExpectDir = (dir == want) ? AGI_DIR_NONE : want;
gWalkPressFrame = gFrame;
hostPress(hostEgoKey(want), 0u, HOST_WALK_HOLD_FRAMES, HOST_WALK_GAP_FRAMES);
}
return false;
}
static void hostWritePpm(const char *path) {
FILE *fp;
fp = fopen(path, "wb");
if (fp == NULL) {
fprintf(stderr, "agiHost: cannot write %s\n", path);
return;
}
fprintf(fp, "P6\n%d %d\n%u\n", JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, HOST_CHANNEL_MAX);
fwrite(gDisplay, 1, sizeof(gDisplay), fp);
fclose(fp);
}
// ----- Port services -----
uint16_t jlpFrameCount(void) {
return (uint16_t)gFrame;
}
uint16_t jlpFrameHz(void) {
return HOST_FRAME_HZ;
}
bool jlpInit(const jlConfigT *config) {
(void)config;
return true;
}
void jlpInputInit(void) {
}
void jlpInputPoll(void) {
uint16_t k;
for (k = 0u; k < (uint16_t)JL_KEY_COUNT; k++) {
jlgKeyState[k] = (uint8_t)((gHeld[k] || gFrame < gHoldUntil[k]) ? 1u : 0u);
}
}
void jlpInputShutdown(void) {
}
void jlpJoystickReset(jlJoystickE js) {
(void)js;
}
void jlpPresent(const jlSurfaceT *src) {
uint16_t x;
uint16_t y;
const uint16_t *pal;
uint8_t byte;
uint8_t nib;
uint16_t rgb;
for (y = 0u; y < JL_SURFACE_HEIGHT; y++) {
pal = src->palette[src->scb[y]];
for (x = 0u; x < JL_SURFACE_WIDTH; x++) {
byte = src->pixels[(uint32_t)y * JL_SURFACE_BYTES_PER_ROW + x / HOST_PIXELS_PER_BYTE];
nib = (uint8_t)((x & 1u) ? (byte & 0x0Fu) : (byte >> 4));
rgb = pal[nib];
gDisplay[y][x][0] = (uint8_t)(((rgb >> 8) & 0x0Fu) * HOST_NIBBLE_SCALE);
gDisplay[y][x][1] = (uint8_t)(((rgb >> 4) & 0x0Fu) * HOST_NIBBLE_SCALE);
gDisplay[y][x][2] = (uint8_t)((rgb & 0x0Fu) * HOST_NIBBLE_SCALE);
}
}
}
void jlpShutdown(void) {
}
void jlpWaitVBL(void) {
gFrame++;
if (gRecording) {
hostRecordFrame();
}
hostStep();
}
// ----- Harness -----
// Read the input script from scriptPath; shots go to outDir ("-" for none).
bool hostScriptOpen(const char *scriptPath, const char *outDir) {
gScript = fopen(scriptPath, "r");
if (gScript == NULL) {
fprintf(stderr, "agiHost: cannot read %s\n", scriptPath);
return false;
}
strncpy(gOutDir, outDir, sizeof(gOutDir) - 1u);
gTrace = getenv("AGI_HOST_TRACE") != NULL;
// Line by line, so a run killed by a timeout still shows how far it got.
setvbuf(stdout, NULL, _IOLBF, 0);
return true;
}