joeyagi/agiSave.c
2026-10-20 17:47:07 -05:00

578 lines
19 KiB
C

// 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 <stddef.h>
#include <stdio.h>
#include <string.h>
#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;
}