386 lines
11 KiB
C
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;
|
|
}
|