joeyagi/agiSave.c
2026-10-06 16:59:41 -05:00

402 lines
13 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.
#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 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;
}