402 lines
13 KiB
C
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;
|
|
}
|