// 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. // // set.simple names the saves itself, as Sierra's interpreter does: save // goes without asking to the slot with that description, else to the // first empty one (with every slot taken by others, a pointer picks one to // overwrite), and restore lists the saves by description for a pointer. // The pointer starts on the newest save (each is numbered one above the // highest on the disk) and wraps at either end of the list. #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 2u #define SAVE_SLOTS 9u #define SAVE_DESC_MAX 30u #define SAVE_NAME_MAX 12u // The pointer lists: their longest header, the box's width (as Sierra's, // whatever the text), and the pointer drawn over the first column of an // entry (" - name"). #define POINTER_HEADER_LINES 4u #define POINTER_BOX_COLS 34 #define POINTER_ON ">" #define POINTER_OFF " " // 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 parm[AGI_OBJ_PARMS]; } SavedObjectT; typedef struct { char magic[3]; uint8_t version; uint32_t sequence; 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; bool holdKey; bool simpleSaveOn; char simpleSaveDesc[AGI_STRING_LEN + 1u]; bool priorityTableSet; uint8_t priorityTable[AGI_HEIGHT]; uint8_t scriptCount; AgiScriptEntryT script[AGI_SCRIPT_MAX]; } SaveFileT; // ----- Prototypes ----- static bool fileBuffer(void); static bool isSimpleDesc(const char *desc); static uint8_t pickSlot(const char *title, bool mustExist); static uint8_t pointerPick(const char *header, bool offerNamed); static uint32_t newestSequence(void); static bool readDescription(uint8_t slot, char *out, uint32_t *sequence); static void simpleDesc(char *out); static uint8_t simpleSlot(void); static void slotName(uint8_t slot, char *out); // ----- Strings ----- // set.simple's dialogs, worded as Sierra's. static const char kSimpleRestore[] = "Use the arrow keys to move\n the pointer to your name.\nThen press ENTER\n"; static const char kSimpleFull[] = " Sorry, this disk is full.\nPosition pointer and press ENTER\n to overwrite a saved game\nor press ESC to cancel.\n"; // ----- 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; } // A save description that is set.simple's. static bool isSimpleDesc(const char *desc) { return strncmp(desc, gAgi->simpleSaveDesc, SAVE_DESC_MAX) == 0; } 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, NULL)) { 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, NULL)) { break; } } } textCloseWindow(); return slot; } // The highest sequence number of the saves on the disk. static uint32_t newestSequence(void) { char desc[SAVE_DESC_MAX + 1u]; uint32_t newest; uint32_t sequence; uint8_t slot; newest = 0u; for (slot = 1u; slot <= SAVE_SLOTS; slot++) { if (readDescription(slot, desc, &sequence) && sequence > newest) { newest = sequence; } } return newest; } // The saves listed under a header, one chosen with a pointer (set.simple's // dialogs), starting on the newest. With offerNamed, set.simple's description is listed too when // no save has it, as slot 0: Sierra's Mother Goose ships a save by that // name ("Start New Character") that starts the game afresh, which // JoeyAGI, keeping its own save files, does by restoring nothing. The // slot, or 0 for that entry or ESC. static uint8_t pointerPick(const char *header, bool offerNamed) { char text[(SAVE_SLOTS + POINTER_HEADER_LINES + 3u) * (AGI_TEXT_COLS + 1u)]; char desc[SAVE_DESC_MAX + 1u]; uint8_t slots[SAVE_SLOTS + 1u]; uint8_t count; uint8_t current; uint8_t first; uint8_t slot; uint16_t key; uint32_t sequence; uint32_t newest; size_t n; bool named; const char *c; // The entries start a line below the header's last. first = 1u; for (c = header; *c != '\0'; c++) { if (*c == '\n') { first++; } } n = (size_t)snprintf(text, sizeof(text), "%s", header); count = 0u; current = 0u; newest = 0u; named = false; for (slot = 1u; slot <= SAVE_SLOTS; slot++) { if (readDescription(slot, desc, &sequence)) { if (sequence > newest) { newest = sequence; current = count; } n += (size_t)snprintf(text + n, sizeof(text) - n, "\n - %s", desc); slots[count++] = slot; named = named || isSimpleDesc(desc); } } if (offerNamed && !named) { simpleDesc(desc); n += (size_t)snprintf(text + n, sizeof(text) - n, "\n - %s", desc); slots[count++] = 0u; } // A blank last line as wide as the box sets its size. (void)snprintf(text + n, sizeof(text) - n, "\n%*s", POINTER_BOX_COLS, ""); if (count == 0u) { return 0u; } textPrint(text, -1, -1, PICK_WIDTH); for (;;) { textWindowPut((uint8_t)(first + current), 0u, POINTER_ON); key = inputWaitKey(); textWindowPut((uint8_t)(first + current), 0u, POINTER_OFF); if (key == AGI_KEY_ESCAPE) { slot = 0u; break; } if (key == AGI_KEY_ENTER) { slot = slots[current]; break; } if (key == AGI_KEY_UP) { current = (uint8_t)((current + count - 1u) % count); } else if (key == AGI_KEY_DOWN) { current = (uint8_t)((current + 1u) % count); } } textCloseWindow(); return slot; } // A slot's description and sequence number (unless NULL), read from the // start of its file; false for no save of this version. static bool readDescription(uint8_t slot, char *out, uint32_t *sequence) { 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 || (uint8_t)head[offsetof(SaveFileT, version)] != SAVE_VERSION) { fclose(fp); return false; } fclose(fp); memcpy(out, &head[offsetof(SaveFileT, description)], SAVE_DESC_MAX + 1u); out[SAVE_DESC_MAX] = '\0'; if (sequence != NULL) { memcpy(sequence, &head[offsetof(SaveFileT, sequence)], sizeof(*sequence)); } return true; } // set.simple's description, cut to a save's. static void simpleDesc(char *out) { const char *end; end = (const char *)memchr(gAgi->simpleSaveDesc, '\0', SAVE_DESC_MAX); memset(out, 0, SAVE_DESC_MAX + 1u); memcpy(out, gAgi->simpleSaveDesc, end != NULL ? (size_t)(end - gAgi->simpleSaveDesc) : SAVE_DESC_MAX); } // set.simple's save slot: the one saved under its description, else the // first empty one; 0 when every slot holds another. static uint8_t simpleSlot(void) { char desc[SAVE_DESC_MAX + 1u]; uint8_t slot; uint8_t fresh; fresh = 0u; for (slot = 1u; slot <= SAVE_SLOTS; slot++) { if (readDescription(slot, desc, NULL)) { if (isSimpleDesc(desc)) { return slot; } } else if (fresh == 0u) { fresh = slot; } } return fresh; } 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; if (gAgi->simpleSaveOn) { slot = pointerPick(kSimpleRestore, true); } else { 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; gAgi->holdKey = gFile->holdKey; gAgi->simpleSaveOn = gFile->simpleSaveOn; memcpy(gAgi->simpleSaveDesc, gFile->simpleSaveDesc, sizeof(gAgi->simpleSaveDesc)); gAgi->priorityTableSet = gFile->priorityTableSet; memcpy(gAgi->priorityTable, gFile->priorityTable, sizeof(gAgi->priorityTable)); // 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; memcpy(o->parm, s->parm, sizeof(o->parm)); } 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; if (gAgi->simpleSaveOn) { slot = simpleSlot(); if (slot == 0u) { slot = pointerPick(kSimpleFull, false); } if (slot == 0u) { return; } simpleDesc(desc); } else { 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; gFile->sequence = newestSequence() + 1u; 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; gFile->holdKey = gAgi->holdKey; gFile->simpleSaveOn = gAgi->simpleSaveOn; memcpy(gFile->simpleSaveDesc, gAgi->simpleSaveDesc, sizeof(gFile->simpleSaveDesc)); gFile->priorityTableSet = gAgi->priorityTableSet; memcpy(gFile->priorityTable, gAgi->priorityTable, sizeof(gFile->priorityTable)); 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; memcpy(s->parm, o->parm, sizeof(s->parm)); } 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; }