// 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 #include #include #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; }