// save.game and restore.game. // // A save holds the interpreter state that is not rebuilt from the game // files: variables, flags, strings, the inventory, the screen objects, // the screen layout and the logics in memory. The picture is not saved; // as in Sierra's interpreter, the commands that built the current room's // picture (draw.pic, overlay.pic, add.to.pic, and the loads) are kept in // a script, which is saved and played back on restore. new.room empties // the script; f7 stops commands being added. // // The files are JoeyLib save files (SAVES/), AGISAVE1..AGISAVE9, in // JoeyAGI's own format: the state as the machine holds it, so a save made // on one kind of machine is for that kind only. #include "agi.h" #include #include #include #include "joey/core.h" #include "joey/draw.h" #include "joey/file.h" #define SAVE_MAGIC_0 'J' #define SAVE_MAGIC_1 'A' #define SAVE_MAGIC_2 'G' #define SAVE_VERSION 1u #define SAVE_SLOTS 9u #define SAVE_DESC_MAX 30u #define SAVE_NAME_MAX 12u // The slot window is as wide as the screen allows with its frame. #define PICK_WIDTH (AGI_TEXT_COLS - 4u) // One saved object: AgiObjectT without its pointers and blit state. typedef struct { uint8_t viewId; uint8_t loop; uint8_t cel; uint8_t priority; int16_t x; int16_t y; uint16_t flags; uint8_t direction; uint8_t stepSize; uint8_t stepTime; uint8_t stepCount; uint8_t cycleTime; uint8_t cycleCount; uint8_t cycleMode; uint8_t motion; uint8_t loopFlag; uint8_t motionFlag; uint8_t targetX; uint8_t targetY; uint8_t savedStepSize; uint8_t randomSteps; uint8_t followDistance; } SavedObjectT; typedef struct { char magic[3]; uint8_t version; char description[SAVE_DESC_MAX + 1u]; uint8_t vars[AGI_NUM_VARS]; uint8_t flags[AGI_NUM_FLAGS]; char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u]; uint8_t itemRoom[AGI_MAX_ITEMS]; AgiKeyMapT keyMaps[AGI_MAX_KEYMAPS]; uint8_t keyMapCount; SavedObjectT objects[AGI_MAX_OBJECTS]; bool logicLoaded[AGI_MAX_RESOURCES]; uint16_t logicScanStart[AGI_MAX_RESOURCES]; bool menuEnabled[AGI_MAX_MENU_ITEMS]; uint8_t horizon; bool blockActive; uint8_t blockX1; uint8_t blockY1; uint8_t blockX2; uint8_t blockY2; bool playerControl; uint8_t picRow; uint8_t inputRow; uint8_t statusRow; bool statusOn; bool inputOn; uint8_t textFg; uint8_t textBg; char cursorChar; uint8_t scriptCount; AgiScriptEntryT script[AGI_SCRIPT_MAX]; } SaveFileT; // ----- Prototypes ----- static bool fileBuffer(void); static uint8_t pickSlot(const char *title, bool mustExist); static bool readDescription(uint8_t slot, char *out); static void slotName(uint8_t slot, char *out); // ----- Module state ----- static SaveFileT *gFile; // ----- Internal helpers (alphabetical) ----- // The save file's image, allocated on first use (it is too big for the // IIgs's bank 0). static bool fileBuffer(void) { if (gFile == NULL) { gFile = (SaveFileT *)jlAlloc(sizeof(SaveFileT)); } return gFile != NULL; } static uint8_t pickSlot(const char *title, bool mustExist) { char text[(SAVE_SLOTS + 3u) * (AGI_TEXT_COLS + 1u)]; char desc[SAVE_DESC_MAX + 1u]; uint8_t slot; uint16_t key; size_t n; // A window listing the slots; a digit picks one. n = (size_t)snprintf(text, sizeof(text), "%s\n\n", title); for (slot = 1u; slot <= SAVE_SLOTS && n < sizeof(text); slot++) { if (!readDescription(slot, desc)) { strcpy(desc, "-"); } n += (size_t)snprintf(text + n, sizeof(text) - n, "%u. %s\n", (unsigned)slot, desc); } if (n < sizeof(text)) { (void)snprintf(text + n, sizeof(text) - n, "\nPress 1-9, or ESC to cancel."); } textPrint(text, -1, -1, PICK_WIDTH); for (;;) { key = inputWaitKey(); if (key == AGI_KEY_ESCAPE) { slot = 0u; break; } if (key >= (uint16_t)'1' && key <= (uint16_t)('0' + SAVE_SLOTS)) { slot = (uint8_t)(key - '0'); if (!mustExist || readDescription(slot, desc)) { break; } } } textCloseWindow(); return slot; } // A slot's description, read from the start of its file. static bool readDescription(uint8_t slot, char *out) { char name[SAVE_NAME_MAX]; FILE *fp; char head[offsetof(SaveFileT, description) + SAVE_DESC_MAX + 1u]; slotName(slot, name); fp = jlSaveOpen(name, "rb"); if (fp == NULL) { return false; } if (fread(head, 1, sizeof(head), fp) != sizeof(head) || head[0] != SAVE_MAGIC_0 || head[1] != SAVE_MAGIC_1 || head[2] != SAVE_MAGIC_2) { fclose(fp); return false; } fclose(fp); memcpy(out, &head[offsetof(SaveFileT, description)], SAVE_DESC_MAX + 1u); out[SAVE_DESC_MAX] = '\0'; return true; } static void slotName(uint8_t slot, char *out) { (void)snprintf(out, SAVE_NAME_MAX, "AGISAVE%u", (unsigned)slot); } // ----- Public API (alphabetical) ----- bool restoreGame(void) { char name[SAVE_NAME_MAX]; uint8_t slot; uint8_t i; uint16_t r; slot = pickSlot("Restore which game?", true); if (slot == 0u) { return false; } if (!fileBuffer()) { return false; } slotName(slot, name); if (jlSaveRead(name, gFile, sizeof(SaveFileT)) != sizeof(SaveFileT) || gFile->version != SAVE_VERSION) { textMessageBox("Error in restoring game."); return false; } soundStop(); gfxEraseAll(); agiResetState(); memcpy(gAgi->vars, gFile->vars, sizeof(gAgi->vars)); memcpy(gAgi->flags, gFile->flags, sizeof(gAgi->flags)); memcpy(gAgi->strings, gFile->strings, sizeof(gAgi->strings)); memcpy(gAgi->itemRoom, gFile->itemRoom, sizeof(gAgi->itemRoom)); memcpy(gAgi->keyMaps, gFile->keyMaps, sizeof(gAgi->keyMaps)); gAgi->keyMapCount = gFile->keyMapCount; for (r = 0u; r < AGI_MAX_RESOURCES; r++) { gAgi->logicLoaded[r] = gFile->logicLoaded[r]; gAgi->logicScanStart[r] = gFile->logicScanStart[r]; } for (i = 0u; i < gAgi->menuItemCount && i < AGI_MAX_MENU_ITEMS; i++) { gAgi->menuItems[i].enabled = gFile->menuEnabled[i]; } gAgi->horizon = gFile->horizon; gAgi->blockActive = gFile->blockActive; gAgi->blockX1 = gFile->blockX1; gAgi->blockY1 = gFile->blockY1; gAgi->blockX2 = gFile->blockX2; gAgi->blockY2 = gFile->blockY2; gAgi->playerControl = gFile->playerControl; gAgi->picRow = gFile->picRow; gAgi->inputRow = gFile->inputRow; gAgi->statusRow = gFile->statusRow; gAgi->statusOn = gFile->statusOn; gAgi->inputOn = gFile->inputOn; gAgi->textFg = gFile->textFg; gAgi->textBg = gFile->textBg; gAgi->cursorChar = gFile->cursorChar; // Rebuild the picture with no objects drawn, then the objects. gAgi->scriptCount = gFile->scriptCount; memcpy(gAgi->script, gFile->script, sizeof(gAgi->script)); for (i = 0u; i < gAgi->scriptCount; i++) { vmReplay(&gAgi->script[i]); } for (i = 0u; i < AGI_MAX_OBJECTS; i++) { const SavedObjectT *s = &gFile->objects[i]; AgiObjectT *o = &gAgi->objects[i]; memset(o, 0, sizeof(*o)); if ((s->flags & OBJ_ANIMATED) != 0u) { (void)objSetView(i, s->viewId); } o->loop = s->loop; o->cel = s->cel; objSetLoop(i, s->loop); objSetCel(i, s->cel); o->priority = s->priority; o->x = s->x; o->y = s->y; o->prevX = s->x; o->prevY = s->y; o->flags = s->flags; o->direction = s->direction; o->stepSize = s->stepSize; o->stepTime = s->stepTime; o->stepCount = s->stepCount; o->cycleTime = s->cycleTime; o->cycleCount = s->cycleCount; o->cycleMode = s->cycleMode; o->motion = s->motion; o->loopFlag = s->loopFlag; o->motionFlag = s->motionFlag; o->targetX = s->targetX; o->targetY = s->targetY; o->savedStepSize = s->savedStepSize; o->randomSteps = s->randomSteps; o->followDistance = s->followDistance; } gfxDrawStatic(); jlSurfaceClear(jlStageGet(), AGI_COLOR_BLACK); gfxShowPic(); textDrawStatus(); textDrawInputLine(); gAgi->flags[FLAG_RESTORED] = 1u; gAgi->restored = true; return true; } void saveGame(void) { char name[SAVE_NAME_MAX]; char desc[SAVE_DESC_MAX + 1u]; uint8_t slot; uint8_t i; uint16_t r; slot = pickSlot("Save in which slot?", false); if (slot == 0u) { return; } desc[0] = '\0'; if (!inputGetString(desc, SAVE_DESC_MAX, AGI_NO_POSITION, AGI_NO_POSITION, "Description: ")) { return; } if (!fileBuffer()) { return; } memset(gFile, 0, sizeof(SaveFileT)); gFile->magic[0] = SAVE_MAGIC_0; gFile->magic[1] = SAVE_MAGIC_1; gFile->magic[2] = SAVE_MAGIC_2; gFile->version = SAVE_VERSION; memcpy(gFile->description, desc, sizeof(desc)); memcpy(gFile->vars, gAgi->vars, sizeof(gFile->vars)); memcpy(gFile->flags, gAgi->flags, sizeof(gFile->flags)); memcpy(gFile->strings, gAgi->strings, sizeof(gFile->strings)); memcpy(gFile->itemRoom, gAgi->itemRoom, sizeof(gFile->itemRoom)); memcpy(gFile->keyMaps, gAgi->keyMaps, sizeof(gFile->keyMaps)); gFile->keyMapCount = gAgi->keyMapCount; for (r = 0u; r < AGI_MAX_RESOURCES; r++) { gFile->logicLoaded[r] = gAgi->logicLoaded[r]; gFile->logicScanStart[r] = gAgi->logicScanStart[r]; } for (i = 0u; i < gAgi->menuItemCount && i < AGI_MAX_MENU_ITEMS; i++) { gFile->menuEnabled[i] = gAgi->menuItems[i].enabled; } gFile->horizon = gAgi->horizon; gFile->blockActive = gAgi->blockActive; gFile->blockX1 = gAgi->blockX1; gFile->blockY1 = gAgi->blockY1; gFile->blockX2 = gAgi->blockX2; gFile->blockY2 = gAgi->blockY2; gFile->playerControl = gAgi->playerControl; gFile->picRow = gAgi->picRow; gFile->inputRow = gAgi->inputRow; gFile->statusRow = gAgi->statusRow; gFile->statusOn = gAgi->statusOn; gFile->inputOn = gAgi->inputOn; gFile->textFg = gAgi->textFg; gFile->textBg = gAgi->textBg; gFile->cursorChar = gAgi->cursorChar; for (i = 0u; i < AGI_MAX_OBJECTS; i++) { const AgiObjectT *o = &gAgi->objects[i]; SavedObjectT *s = &gFile->objects[i]; s->viewId = o->viewId; s->loop = o->loop; s->cel = o->cel; s->priority = o->priority; s->x = o->x; s->y = o->y; s->flags = o->flags; s->direction = o->direction; s->stepSize = o->stepSize; s->stepTime = o->stepTime; s->stepCount = o->stepCount; s->cycleTime = o->cycleTime; s->cycleCount = o->cycleCount; s->cycleMode = o->cycleMode; s->motion = o->motion; s->loopFlag = o->loopFlag; s->motionFlag = o->motionFlag; s->targetX = o->targetX; s->targetY = o->targetY; s->savedStepSize = o->savedStepSize; s->randomSteps = o->randomSteps; s->followDistance = o->followDistance; } gFile->scriptCount = gAgi->scriptCount; memcpy(gFile->script, gAgi->script, sizeof(gFile->script)); slotName(slot, name); if (!jlSaveWrite(name, gFile, sizeof(SaveFileT))) { textMessageBox("The game could not be saved."); } } // Remember a picture-building command for restore (unless f7 blocks it). void saveScriptAdd(uint8_t op, uint8_t arg) { uint8_t args[AGI_SCRIPT_ARGS]; memset(args, 0, sizeof(args)); args[0] = arg; saveScriptAddArgs(op, args); } void saveScriptAddArgs(uint8_t op, const uint8_t *args) { AgiScriptEntryT *e; if (gAgi->flags[FLAG_NO_SCRIPT] != 0u || gAgi->scriptCount >= AGI_SCRIPT_MAX) { return; } e = &gAgi->script[gAgi->scriptCount]; e->op = op; memcpy(e->args, args, AGI_SCRIPT_ARGS); gAgi->scriptCount++; } // new.room: the new room's picture starts from nothing. void saveScriptClear(void) { gAgi->scriptCount = 0u; }