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

386 lines
11 KiB
C

// JoeyAGI's start-up and the interpreter cycle (AGI specification 3.1).
//
// Every v10 twentieths of a second the interpreter runs one cycle: the
// controllers and f2/f4 are cleared and the queued keys handed out, the
// special motions pick their directions, logic 0 runs (and with it every
// logic it calls), the variables the interpreter reports are reset, and
// the updated objects animate and move. new.room stops all logic,
// changes room and runs logic 0 again within the same cycle.
//
// Between cycles, and while a window or menu waits for a key, agiIdle
// runs a frame: input, sound, the clock (v11..v14) and the display.
#include "agi.h"
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "joey/audio.h"
#include "joey/core.h"
#include "joey/debug.h"
#include "joey/draw.h"
#include "joey/input.h"
#include "joey/palette.h"
#include "joey/present.h"
#define CYCLE_UNIT_MS 50u
#define MS_PER_SECOND 1000u
#define SECONDS_PER_MINUTE 60u
#define MINUTES_PER_HOUR 60u
#define HOURS_PER_DAY 24u
#define DEFAULT_CYCLE_DELAY 2u
#define DEFAULT_INPUT_MAX 41u
#define DEFAULT_FREE_PAGES 100u
#define DEFAULT_PIC_ROW 0u
#define DEFAULT_INPUT_ROW 23u
#define DEFAULT_STATUS_ROW 21u
#define COMPUTER_IBM_PC 0u
#define SOUND_PC 1u
#define MONITOR_EGA 3u
#define RANDOM_SEED 0xA61DA61Du
#define STATUS_TEXT_MAX 160u
// The AGI palette: EGA's sixteen colours in JoeyLib's $0RGB.
static const uint16_t kAgiPalette[AGI_COLOR_COUNT] = {
0x000u, 0x00Au, 0x0A0u, 0x0AAu,
0xA00u, 0xA0Au, 0xA50u, 0xAAAu,
0x555u, 0x55Fu, 0x5F5u, 0x5FFu,
0xF55u, 0xF5Fu, 0xFF5u, 0xFFFu
};
// ----- Prototypes -----
static void changeRoom(void);
static void cycle(void);
static void steerEgo(AgiObjectT *ego);
static void tickClock(void);
// ----- Module state -----
AgiStateT *gAgi;
static uint32_t gClockMs;
static uint32_t gClockLastMs;
// ----- Internal helpers (alphabetical) -----
// new.room's work (specification 3.1 and 4.2): 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;
objUnanimateAll();
vmDiscardLogics();
agiViewDiscardUnused();
saveScriptClear();
gAgi->playerControl = true;
gAgi->blockActive = false;
gAgi->horizon = AGI_DEFAULT_HORIZON;
gAgi->vars[VAR_PREV_ROOM] = gAgi->vars[VAR_ROOM];
gAgi->vars[VAR_ROOM] = gAgi->newRoomNumber;
gAgi->vars[VAR_OBJ_EDGE_OBJ] = 0u;
gAgi->vars[VAR_OBJ_EDGE] = 0u;
gAgi->vars[VAR_BAD_WORD] = 0u;
ego = &gAgi->objects[AGI_EGO];
gAgi->vars[VAR_EGO_VIEW] = ego->viewId;
ego->direction = AGI_DIR_NONE;
ego->motion = AGI_MOTION_NORMAL;
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
switch (gAgi->vars[VAR_EGO_EDGE]) {
case AGI_EDGE_TOP:
ego->y = AGI_MAX_Y;
break;
case AGI_EDGE_RIGHT:
ego->x = 0;
break;
case AGI_EDGE_BOTTOM:
ego->y = (int16_t)(AGI_DEFAULT_HORIZON + 1u);
break;
case AGI_EDGE_LEFT:
ego->x = (int16_t)(AGI_WIDTH - (int16_t)ego->width);
break;
default:
break;
}
gAgi->vars[VAR_EGO_EDGE] = AGI_EDGE_NONE;
(void)vmLoadLogic(gAgi->vars[VAR_ROOM]);
gAgi->logicLoaded[gAgi->vars[VAR_ROOM]] = true;
gAgi->flags[FLAG_ENTERED] = 0u;
gAgi->flags[FLAG_NEW_ROOM] = 1u;
inputReset();
}
static void cycle(void) {
AgiObjectT *ego;
uint8_t chosen;
memset(gAgi->controllers, 0, sizeof(gAgi->controllers));
gAgi->flags[FLAG_ENTERED] = 0u;
gAgi->flags[FLAG_SAID_OK] = 0u;
gAgi->vars[VAR_BAD_WORD] = 0u;
if (gAgi->menuRequested) {
gAgi->menuRequested = false;
chosen = textMenuInput();
if (chosen != AGI_NO_CONTROLLER) {
gAgi->controllers[chosen] = 1u;
}
}
inputProcess();
// v6 steers ego, under player and program control alike, unless
// move.obj, wander or follow.ego has it: before logic 0 and again
// after it (so v6 set by a logic moves ego this cycle and set.dir on
// ego never does).
ego = &gAgi->objects[AGI_EGO];
steerEgo(ego);
objPlanMotion();
textUpdateStatus();
for (;;) {
gAgi->abortLogic = false;
vmCallLogic(0u);
if (!gAgi->newRoomPending) {
break;
}
changeRoom();
}
gAgi->abortLogic = false;
// A restore ends the cycle as it was saved; logic 0 sees f12 next.
if (gAgi->restored) {
gAgi->restored = false;
return;
}
steerEgo(ego);
textUpdateStatus();
gAgi->vars[VAR_OBJ_EDGE_OBJ] = 0u;
gAgi->vars[VAR_OBJ_EDGE] = 0u;
gAgi->flags[FLAG_NEW_ROOM] = 0u;
gAgi->flags[FLAG_RESTART] = 0u;
gAgi->flags[FLAG_RESTORED] = 0u;
gAgi->flags[FLAG_FIRST_LOGIC0] = 0u;
if (!gAgi->textMode) {
objUpdateAll();
gfxPresentDirty();
}
}
static void steerEgo(AgiObjectT *ego) {
if (ego->motion == AGI_MOTION_NORMAL) {
ego->direction = gAgi->vars[VAR_EGO_DIR];
}
}
// v11..v14 count real time.
static void tickClock(void) {
uint32_t now;
now = jlMillisElapsed();
gClockMs = gClockMs + (now - gClockLastMs);
gClockLastMs = now;
while (gClockMs >= MS_PER_SECOND) {
gClockMs -= MS_PER_SECOND;
gAgi->vars[VAR_SECONDS]++;
if (gAgi->vars[VAR_SECONDS] < SECONDS_PER_MINUTE) {
continue;
}
gAgi->vars[VAR_SECONDS] = 0u;
gAgi->vars[VAR_MINUTES]++;
if (gAgi->vars[VAR_MINUTES] < MINUTES_PER_HOUR) {
continue;
}
gAgi->vars[VAR_MINUTES] = 0u;
gAgi->vars[VAR_HOURS]++;
if (gAgi->vars[VAR_HOURS] < HOURS_PER_DAY) {
continue;
}
gAgi->vars[VAR_HOURS] = 0u;
gAgi->vars[VAR_DAYS]++;
}
}
// ----- Public API (alphabetical) -----
// One frame of everything that runs while the game waits: the frame
// passes first, so the keys, the sound and the clock are up to date when
// a cycle starts right after it (as the PC's timer interrupt keeps them).
void agiIdle(void) {
jlWaitVBL();
inputPoll();
soundPump();
tickClock();
jlStagePresent();
jlAudioFrameTick();
}
// new.room: logic stops; the room changes before logic 0 runs again.
void agiNewRoom(uint8_t room) {
gAgi->newRoomPending = true;
gAgi->newRoomNumber = room;
gAgi->abortLogic = true;
}
// obj.status.v: an object's numbers, for debugging.
void agiObjectStatus(uint8_t n) {
char text[STATUS_TEXT_MAX];
const AgiObjectT *o;
if (n >= gAgi->objectCount) {
return;
}
o = &gAgi->objects[n];
snprintf(text, sizeof(text), "Object %u:\nx: %d y: %d pri: %u\nstepsize: %u",
(unsigned)n, (int)o->x, (int)o->y, (unsigned)o->priority, (unsigned)o->stepSize);
textMessageBox(text);
}
// quit: at once, or after asking.
void agiQuit(bool immediately) {
if (!immediately) {
textPrint("Press ENTER to quit.\nPress ESC to keep playing.", -1, -1, 0u);
if (!inputWaitPrintClose(0u)) {
textCloseWindow();
return;
}
textCloseWindow();
}
gAgi->quitRequested = true;
gAgi->abortLogic = true;
}
// Everything as it is when the game starts -- but the menus and the
// set.key mappings, which a game makes once: KQ3 skips both when it is
// restarted (f6), and Sierra's interpreter keeps them.
void agiResetState(void) {
uint16_t i;
memset(gAgi->vars, 0, sizeof(gAgi->vars));
memset(gAgi->flags, 0, sizeof(gAgi->flags));
memset(gAgi->strings, 0, sizeof(gAgi->strings));
memset(gAgi->controllers, 0, sizeof(gAgi->controllers));
vmDiscardLogics();
gAgi->logicLoaded[0] = true;
gAgi->logicScanStart[0] = 0u;
objClearAll();
for (i = 0u; i < AGI_MAX_ITEMS; i++) {
gAgi->itemRoom[i] = agiItemStartRoom(&gAgi->game, (uint8_t)i);
}
gAgi->menuRequested = false;
gAgi->horizon = AGI_DEFAULT_HORIZON;
gAgi->blockActive = false;
gAgi->playerControl = true;
gAgi->picRow = DEFAULT_PIC_ROW;
gAgi->inputRow = DEFAULT_INPUT_ROW;
gAgi->statusRow = DEFAULT_STATUS_ROW;
gAgi->statusOn = false;
gAgi->inputOn = false;
gAgi->textMode = false;
gAgi->textFg = AGI_COLOR_WHITE;
gAgi->textBg = AGI_COLOR_BLACK;
gAgi->cursorChar = AGI_NO_CURSOR;
gAgi->windowOpen = false;
gAgi->inputLine[0] = '\0';
gAgi->inputLen = 0u;
gAgi->lastLine[0] = '\0';
gAgi->wordCount = 0u;
gAgi->newRoomPending = false;
gAgi->abortLogic = false;
gAgi->vars[VAR_CYCLE_DELAY] = DEFAULT_CYCLE_DELAY;
gAgi->vars[VAR_INPUT_MAX] = DEFAULT_INPUT_MAX;
gAgi->vars[VAR_FREE_PAGES] = DEFAULT_FREE_PAGES;
gAgi->vars[VAR_COMPUTER] = COMPUTER_IBM_PC;
gAgi->vars[VAR_SOUND_TYPE] = SOUND_PC;
gAgi->vars[VAR_MONITOR] = MONITOR_EGA;
gAgi->flags[FLAG_FIRST_LOGIC0] = 1u;
gAgi->flags[FLAG_SOUND] = 1u;
saveScriptClear();
inputReset();
}
// restart.game: after asking, start over with f6 set.
void agiRestartGame(void) {
textPrint("Press ENTER to restart\nthe game.\n\nPress ESC to continue\nthis game.", -1, -1, 0u);
if (!inputWaitPrintClose(0u)) {
textCloseWindow();
return;
}
textCloseWindow();
gAgi->restartRequested = true;
gAgi->abortLogic = true;
}
// ----- Entry point -----
int main(int argc, char **argv) {
jlConfigT cfg;
uint32_t lastCycleMs;
uint32_t now;
bool soundWasOn;
(void)argc;
(void)argv;
memset(&cfg, 0, sizeof(cfg));
if (!jlInit(&cfg)) {
return 1;
}
jlLogReset();
jlPaletteSet(jlStageGet(), 0u, kAgiPalette);
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
jlRandomSeed(RANDOM_SEED);
gAgi = (AgiStateT *)jlAlloc(sizeof(AgiStateT));
if (gAgi == NULL) {
jlShutdown();
return 1;
}
memset(gAgi, 0, sizeof(AgiStateT));
if (!agiResOpen(&gAgi->game) || !gfxInit()) {
jlLog("AGI: the game's files are missing from DATA/");
jlLogFlush();
jlShutdown();
return 1;
}
gAgi->objectCount = gAgi->game.maxObjects;
soundInit();
agiResetState();
gClockLastMs = jlMillisElapsed();
lastCycleMs = gClockLastMs;
while (!gAgi->quitRequested) {
cycle();
if (gAgi->restartRequested) {
soundWasOn = gAgi->flags[FLAG_SOUND] != 0u;
soundStop();
gfxEraseAll();
agiResetState();
gAgi->restartRequested = false;
gAgi->flags[FLAG_RESTART] = 1u;
gAgi->flags[FLAG_SOUND] = soundWasOn ? 1u : 0u;
jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK);
}
// The next cycle starts v10 twentieths of a second after this one.
do {
agiIdle();
now = jlMillisElapsed();
} while (now - lastCycleMs < (uint32_t)gAgi->vars[VAR_CYCLE_DELAY] * CYCLE_UNIT_MS && !gAgi->quitRequested);
lastCycleMs = now;
}
soundShutdown();
gfxFree();
agiResClose(&gAgi->game);
jlFree(gAgi);
jlShutdown();
return 0;
}