// 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 // mouse X Y attach a mouse, its pointer at screen pixel // (X, Y) (the host has none until then) // click press the left button as long as 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 #include #include #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 void hostVmTrace(uint8_t logic, uint16_t pc, uint8_t op, const uint8_t *args, uint8_t argCount, int8_t result); 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 uint32_t gClickUntil; 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; // AGI_HOST_VM_TRACE=FILE: every command and condition the VM runs, one line // each (hostVmTrace). static FILE *gVmTrace; 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, "mouse") == 0) { char *yArg; yArg = arg + strcspn(arg, " \t"); if (*yArg == '\0') { fprintf(stderr, "agiHost: mouse wants X Y in '%s'\n", gOpLine); exit(2); } jlgMouseAttached = true; jlgMouseX = (int16_t)atoi(arg); jlgMouseY = (int16_t)atoi(yArg + 1); } else if (strcmp(op, "click") == 0) { gClickUntil = gFrame + HOST_KEY_HOLD_FRAMES; gResumeFrame = gFrame + HOST_KEY_HOLD_FRAMES + HOST_KEY_GAP_FRAMES; } 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. // One line of the VM trace: the time, the logic, the command's offset in // it, the command (FF for a condition, with its result) and its arguments. static void hostVmTrace(uint8_t logic, uint16_t pc, uint8_t op, const uint8_t *args, uint8_t argCount, int8_t result) { uint8_t i; fprintf(gVmTrace, "%7.2f L%03u @%05u %02X", (double)gFrame / (double)HOST_FRAME_HZ, (unsigned)logic, (unsigned)pc, (unsigned)op); if (result >= 0) { fprintf(gVmTrace, " = %d", (int)result); } for (i = 0u; i < argCount; i++) { fprintf(gVmTrace, " %u", (unsigned)args[i]); } fputc('\n', gVmTrace); } 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); } jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (uint8_t)((gFrame < gClickUntil) ? 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; if (getenv("AGI_HOST_VM_TRACE") != NULL) { gVmTrace = fopen(getenv("AGI_HOST_VM_TRACE"), "w"); if (gVmTrace != NULL) { gAgiTrace = hostVmTrace; } } // Line by line, so a run killed by a timeout still shows how far it got. setvbuf(stdout, NULL, _IOLBF, 0); return true; }