diff --git a/Makefile b/Makefile index 3c0cb3e..83c46d1 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # interpreter to them. # # make every target whose toolchain is installed -# make one target: iigs amiga atarist dos x68000 dvx +# make one target: iigs amiga atarist dos x68000 dvx sdl3 # make iigs-game-disk-agi the bootable IIgs volume with a game on it # (games/kq3; AGI_GAME=NAME for games/NAME) # make check build, then run the gates (scripts/verify.sh) diff --git a/agi.c b/agi.c index 1681a2d..648a4bd 100644 --- a/agi.c +++ b/agi.c @@ -69,13 +69,15 @@ static uint32_t gClockLastMs; // ----- Internal helpers (alphabetical) ----- -// new.room's work (specification 3.1 and 4.2): everything off the +// new.room's work (specification 3.1 and 4.2): the sound stopped (its +// flag set at once, as Sierra's interpreter does), everything off the // screen, the logics forgotten, the player in control, ego moved to the // edge it left by, and the new room's logic loaded. static void changeRoom(void) { AgiObjectT *ego; gAgi->newRoomPending = false; + soundStop(); objUnanimateAll(); vmDiscardLogics(); agiViewDiscardUnused(); diff --git a/agiVm.c b/agiVm.c index e767e02..68ed0c1 100644 --- a/agiVm.c +++ b/agiVm.c @@ -631,7 +631,12 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) { d = DISTANCE_FAR; if (o != NULL && p != NULL && (o->flags & OBJ_DRAWN) != 0u && (p->flags & OBJ_DRAWN) != 0u) { - d = (uint16_t)(((o->x > p->x) ? o->x - p->x : p->x - o->x) + ((o->y > p->y) ? o->y - p->y : p->y - o->y)); + // From the middle of one baseline to the middle of the + // other, as Sierra's interpreter measures it. + int16_t ox = (int16_t)(o->x + o->width / 2u); + int16_t px = (int16_t)(p->x + p->width / 2u); + + d = (uint16_t)(((ox > px) ? ox - px : px - ox) + ((o->y > p->y) ? o->y - p->y : p->y - o->y)); if (d > DISTANCE_MAX) { d = DISTANCE_MAX; } diff --git a/tests/expected/probes/distance.txt b/tests/expected/probes/distance.txt new file mode 100644 index 0000000..74d44dd --- /dev/null +++ b/tests/expected/probes/distance.txt @@ -0,0 +1 @@ +a 4f6182a8 diff --git a/tests/expected/probes/egoblocked.txt b/tests/expected/probes/egoblocked.txt new file mode 100644 index 0000000..3141586 --- /dev/null +++ b/tests/expected/probes/egoblocked.txt @@ -0,0 +1 @@ +a 299bf80c diff --git a/tests/expected/probes/newroom.txt b/tests/expected/probes/newroom.txt new file mode 100644 index 0000000..43a1c3c --- /dev/null +++ b/tests/expected/probes/newroom.txt @@ -0,0 +1 @@ +a eecb58af diff --git a/tests/expected/probes/printkeys.txt b/tests/expected/probes/printkeys.txt new file mode 100644 index 0000000..9f9abf1 --- /dev/null +++ b/tests/expected/probes/printkeys.txt @@ -0,0 +1,5 @@ +a 4f293179 +b 4f293179 +c 2b4619ed +d 2b4619ed +e 7662eaad diff --git a/tests/hostPort.c b/tests/hostPort.c index 1cb2d06..0c44847 100644 --- a/tests/hostPort.c +++ b/tests/hostPort.c @@ -12,12 +12,39 @@ // 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, @@ -48,6 +75,7 @@ #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 @@ -55,6 +83,29 @@ #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 { @@ -63,15 +114,69 @@ typedef struct { 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); +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); @@ -108,6 +213,9 @@ static const HostKeyNameT kKeyNames[] = { { "exclam", 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; @@ -119,6 +227,7 @@ static uint8_t gDisplay[SURFACE_HEIGHT][SURFACE_WIDTH][3]; 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. @@ -129,10 +238,142 @@ 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(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]; @@ -145,14 +386,66 @@ static void hostDump(const char *name) { } -static uint32_t hostFrameHash(void) { - const uint8_t *p; - uint32_t hash; - size_t i; +// Go on from the line after 'label NAME'. +static void hostGoto(const char *label) { + char line[HOST_LINE_MAX]; + char *p; - hash = FNV_OFFSET_BASIS; - p = &gDisplay[0][0][0]; - for (i = 0; i < sizeof(gDisplay); i++) { + 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 KEY_RIGHT; + case AGI_DIR_S: + return KEY_DOWN; + case AGI_DIR_W: + return KEY_LEFT; + default: + return 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; @@ -197,7 +490,7 @@ static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh) { } -static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) { +static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames) { if (key != KEY_NONE) { gHoldUntil[key] = gFrame + holdFrames; } @@ -205,7 +498,7 @@ static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) { if (ch != 0u && !gHeld[KEY_LALT] && !gHeld[KEY_LCTRL]) { jlInputCharPush(ch); } - gResumeFrame = gFrame + holdFrames + HOST_KEY_GAP_FRAMES; + gResumeFrame = gFrame + holdFrames + gapFrames; } @@ -213,13 +506,15 @@ static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) { // 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++) { - gRecordHash = (gRecordHash ^ ((hash >> (i * BYTE_BITS)) & BYTE_MASK)) * FNV_PRIME; + bytes[i] = (uint8_t)((hash >> (i * BYTE_BITS)) & BYTE_MASK); } + gRecordHash = hostHashBytes(gRecordHash, bytes, HASH_BYTES); gRecordFrames++; if (gRecordLog == NULL) { return; @@ -239,6 +534,83 @@ static void hostRecordFrame(void) { } +// 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]; @@ -249,6 +621,13 @@ static void hostStep(void) { 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]; @@ -256,9 +635,25 @@ static void hostStep(void) { one[1] = '\0'; gTypePos++; if (one[0] == ' ') { - hostPress(KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES); + hostPress(KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES); } else if (hostKeyLookup(one, &key, &ch)) { - hostPress(key, ch, HOST_KEY_HOLD_FRAMES); + hostPress(key, ch, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES); + } + continue; + } + if (gSay == HOST_SAY_TYPING) { + hostPress(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; } @@ -268,12 +663,15 @@ static void hostStep(void) { exit(0); } line[strcspn(line, "\r\n")] = '\0'; + strcpy(gOpLine, line); op = line; while (*op == ' ' || *op == '\t') { op++; } - if (strncmp(op, "type ", 5) == 0) { - strncpy(gTypeBuf, op + 5, sizeof(gTypeBuf) - 1u); + 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; @@ -290,6 +688,7 @@ static void hostStep(void) { 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); @@ -307,7 +706,7 @@ static void hostStep(void) { 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); + 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 == KEY_NONE) { fprintf(stderr, "agiHost: unknown key '%s'\n", arg); @@ -338,16 +737,89 @@ static void hostStep(void) { gRecordLog = NULL; } printf("[%6.1f] shot %s %08x\n", (double)gFrame / (double)HOST_FRAME_HZ, gRecordName, (unsigned)gRecordHash); - } else if (strcmp(op, "clear") == 0) { - // Dismiss message windows: Return while one is open, a few - // times at most (a window that wants another key stays). - if (gAgi != NULL && gAgi->windowOpen && gClearTries < HOST_CLEAR_TRIES) { - gClearTries++; - hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES); - gResumeFrame = gFrame + HOST_CLEAR_GAP_FRAMES; - fseek(gScript, gLinePos, SEEK_SET); + } 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 { - gClearTries = 0u; + 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); @@ -357,6 +829,215 @@ static void hostStep(void) { } +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(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 towards 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; @@ -456,6 +1137,7 @@ bool hostScriptOpen(const char *scriptPath, const char *outDir) { 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; diff --git a/tests/probes.py b/tests/probes.py index 7ec09d6..ca4172a 100755 --- a/tests/probes.py +++ b/tests/probes.py @@ -1212,6 +1212,167 @@ def probeCursor(): return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}), script +def probeEgoBlocked(): + # Ego walked into a barrier with the arrow keys: does it keep its + # direction (v6 and get.dir) or stop? Shown a few cycles after it hit. + pic = bytes([PIC_COLOR, 0, PIC_PRIORITY, 0, PIC_LINE, 40, 0, 40, 100, PIC_END]) + + def body(L): + with L.If(("isset", F_NEW_ROOM)): + L.assignn(V_CYCLE_DELAY, 2) + showPic(L) + L.load_view(0) + L.animate_obj(0) + L.set_view(0, 0) + L.position(0, 60, 80) + L.draw(0) + L.show_pic() + with L.If(("have.key",)): + L.get_posn(0, 194, 195) + L.get_dir(0, 196) + L.display(1, 0, L.m("at %v194,%v195 v6 %v6 dir %v196")) + script = "wait 2\nkey Left\nwait 6\nkey a\nwait 1\nshot a\n" + return dict(logics=roomLogic(body), pics={1: pic}, views=VIEWS), script + + +def probePrintKeys(): + # Keys pressed while a print window waits: a letter (does it close the + # window, or reach the input line once it has closed?), then Return; + # then the same over a window that closes by itself (v21). + def body(L): + with L.If(("isset", F_NEW_ROOM)): + L.assignn(V_CYCLE_DELAY, 2) + showPic(L) + L.show_pic() + L.set_cursor_char(L.m("_")) + L.accept_input() + L.increment(V_CYCLE) + with L.If(("equaln", V_CYCLE, 5)): + L.print(L.m("A window waiting for a key.")) + # Cycles stop while the first window waits: this one opens as + # soon as it is closed. + with L.If(("equaln", V_CYCLE, 6)): + L.assignn(V_PRINT_TIMEOUT, 6) + L.print(L.m("A window for three seconds.")) + script = ("wait 2\nkey a\nwait 0.5\nshot a\nkey b\nwait 0.5\nshot b\nkey Return\nwait 1\nshot c\n" + "key d\nwait 0.5\nshot d\nwait 4\nshot e\n") + return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}), script + + +def probeEdgeWalk(): + # Ego walked off the right edge with the arrow keys. In the next room: + # where new.room put it (cycle 1); where it is and which way it goes 6 + # cycles later, not yet animated again; and 6 cycles after an + # animate.obj (KQ3 animates ego in every new room); and after Right is + # pressed again there. + def body(L): + with L.If(("isset", F_NEW_ROOM)): + L.assignn(V_CYCLE_DELAY, 2) + showPic(L) + L.load_view(0) + L.animate_obj(0) + L.set_view(0, 0) + L.position(0, 140, 100) + L.draw(0) + L.show_pic() + with L.If(("!", "equaln", V_EGO_EDGE, 0)): + L.new_room(PROBE_ROOM + 1) + logics = roomLogic(body) + L = Logic() + with L.If(("isset", F_NEW_ROOM)): + showPic(L) + L.load_view(0) + L.set_view(0, 0) + L.draw(0) + L.show_pic() + L.increment(V_CYCLE + 1) + with L.If(("equaln", V_CYCLE + 1, 1)): + L.get_posn(0, 220, 221) + with L.If(("equaln", V_CYCLE + 1, 7)): + L.get_posn(0, 222, 223) + L.get_dir(0, 224) + L.assignv(225, V_EGO_DIR) + L.animate_obj(0) + with L.If(("equaln", V_CYCLE + 1, 13)): + L.get_posn(0, 226, 227) + L.get_dir(0, 228) + L.assignv(229, V_EGO_DIR) + with L.If(("equaln", V_CYCLE + 1, 30)): + L.get_posn(0, 230, 231) + L.get_dir(0, 232) + L.assignv(233, V_EGO_DIR) + L.display(1, 0, L.m("in at %v220,%v221")) + L.display(2, 0, L.m("then %v222,%v223 dir %v224 v6 %v225")) + L.display(3, 0, L.m("animated %v226,%v227 dir %v228 v6 %v229")) + L.display(4, 0, L.m("pressed %v230,%v231 dir %v232 v6 %v233")) + L.return_() + logics[PROBE_ROOM + 1] = L.build() + return dict(logics=logics, pics={1: EMPTY_PIC}, views=VIEWS), "wait 2\nkey Right\nwait 3.5\nkey Right\nwait 3\nshot a\n" + + +def probeNewRoom(): + # A sound still playing when the room changes: a 2-second tune started + # with f60 three cycles before new.room. Shown: the cycle in the new + # room at which f60 was first seen set (1: at once, 0: never). + tune = {3: sound([[(120, 254, 2)]])} + + def body(L): + with L.If(("isset", F_NEW_ROOM)): + L.assignn(V_CYCLE_DELAY, 1) + showPic(L) + L.show_pic() + L.load_sound(3) + L.increment(V_CYCLE) + with L.If(("equaln", V_CYCLE, 3)): + L.sound(3, 60) + with L.If(("equaln", V_CYCLE, 6)): + L.new_room(PROBE_ROOM + 1) + logics = roomLogic(body) + L = Logic() + with L.If(("isset", F_NEW_ROOM)): + showPic(L) + L.show_pic() + L.increment(V_CYCLE + 1) + with L.If(("isset", 60), ("equaln", 220, 0)): + L.assignv(220, V_CYCLE + 1) + with L.If(("equaln", V_CYCLE + 1, 80)): + L.display(1, 0, L.m("flag at %v220")) + L.return_() + logics[PROBE_ROOM + 1] = L.build() + return dict(logics=logics, pics={1: EMPTY_PIC}, sounds=tune), "wait 6\nshot a\n" + + +def probeDistance(): + # distance between objects of different widths: side by side, one + # above the other, apart both ways (each way round), and to an object + # that is not drawn. + places = [(1, 0, 20, 100), (2, 1, 40, 100), (3, 0, 60, 80), (4, 0, 60, 100), (5, 1, 100, 130)] + + def body(L): + with L.If(("isset", F_NEW_ROOM)): + showPic(L) + L.load_view(0) + L.load_view(1) + for o, v, x, y in places: + L.animate_obj(o) + L.set_view(o, v) + L.position(o, x, y) + L.ignore_objs(o) + L.stop_cycling(o) + L.draw(o) + L.animate_obj(6) + L.set_view(6, 0) + L.position(6, 120, 60) + L.show_pic() + L.increment(V_CYCLE) + with L.If(("equaln", V_CYCLE, 3)): + for i, (a, b) in enumerate(((1, 2), (2, 1), (3, 4), (1, 5), (5, 1), (1, 6))): + L.distance(a, b, 220 + i) + L.display(1, 0, L.m("1-2 %v220 2-1 %v221 3-4 %v222")) + L.display(2, 0, L.m("1-5 %v223 5-1 %v224 1-6 %v225")) + return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}, views=VIEWS), "wait 3\nshot a\n" + + PROBES = { "font": probeFont, "pictures": probePictures, @@ -1236,6 +1397,11 @@ PROBES = { "shake": probeShake, "winedge": probeWindowEdge, "cursor": probeCursor, + "egoblocked": probeEgoBlocked, + "printkeys": probePrintKeys, + "newroom": probeNewRoom, + "edgewalk": probeEdgeWalk, + "distance": probeDistance, }