1144 lines
41 KiB
C
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", KEY_RETURN, JL_CHAR_RETURN },
|
|
{ "Escape", KEY_ESCAPE, JL_CHAR_ESCAPE },
|
|
{ "Tab", KEY_TAB, JL_CHAR_TAB },
|
|
{ "BackSpace", KEY_BACKSPACE, JL_CHAR_BACKSPACE },
|
|
{ "space", KEY_SPACE, ' ' },
|
|
{ "Up", KEY_UP, 0u },
|
|
{ "Down", KEY_DOWN, 0u },
|
|
{ "Left", KEY_LEFT, 0u },
|
|
{ "Right", KEY_RIGHT, 0u },
|
|
{ "F1", KEY_F1, 0u },
|
|
{ "F2", KEY_F2, 0u },
|
|
{ "F3", KEY_F3, 0u },
|
|
{ "F4", KEY_F4, 0u },
|
|
{ "F5", KEY_F5, 0u },
|
|
{ "F6", KEY_F6, 0u },
|
|
{ "F7", KEY_F7, 0u },
|
|
{ "F8", KEY_F8, 0u },
|
|
{ "F9", KEY_F9, 0u },
|
|
{ "F10", KEY_F10, 0u },
|
|
{ "ctrl", KEY_LCTRL, 0u },
|
|
{ "alt", KEY_LALT, 0u },
|
|
{ "shift", KEY_LSHIFT, 0u },
|
|
{ "period", KEY_NONE, '.' },
|
|
{ "comma", KEY_NONE, ',' },
|
|
{ "apostrophe", KEY_NONE, '\'' },
|
|
{ "minus", KEY_NONE, '-' },
|
|
{ "question", KEY_NONE, '?' },
|
|
{ "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;
|
|
static uint32_t gResumeFrame;
|
|
static uint32_t gHoldUntil[KEY_COUNT];
|
|
static bool gHeld[KEY_COUNT];
|
|
static uint8_t gDisplay[SURFACE_HEIGHT][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(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 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;
|
|
}
|
|
|
|
|
|
// 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)(KEY_A + (c - 'a'));
|
|
*outCh = (uint8_t)c;
|
|
return true;
|
|
}
|
|
if (c >= 'A' && c <= 'Z') {
|
|
*outKey = (jlKeyE)(KEY_A + (c - 'A'));
|
|
*outCh = (uint8_t)c;
|
|
return true;
|
|
}
|
|
if (c >= '0' && c <= '9') {
|
|
*outKey = (jlKeyE)(KEY_0 + (c - '0'));
|
|
*outCh = (uint8_t)c;
|
|
return true;
|
|
}
|
|
*outKey = KEY_NONE;
|
|
*outCh = (uint8_t)c;
|
|
return true;
|
|
}
|
|
|
|
|
|
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames) {
|
|
if (key != KEY_NONE) {
|
|
gHoldUntil[key] = gFrame + holdFrames;
|
|
}
|
|
// Like the real keyboards, a Ctrl or Alt chord types no character.
|
|
if (ch != 0u && !gHeld[KEY_LALT] && !gHeld[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(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(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 == 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(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;
|
|
|
|
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", SURFACE_WIDTH, 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)KEY_COUNT; k++) {
|
|
gKeyState[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 < SURFACE_HEIGHT; y++) {
|
|
pal = src->palette[src->scb[y]];
|
|
for (x = 0u; x < SURFACE_WIDTH; x++) {
|
|
byte = src->pixels[(uint32_t)y * 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;
|
|
}
|