Saved games
A ProDOS write is not atomic. A power loss or a disk error can leave a file that is only partly written. Write the saves to two files in turn. Load the newer file that is valid.
Two slots
- Keep the save in one small structure with a marker, a sequence number and a checksum.
- Write each save to the file that does not hold the newest valid save. Thus a failed write does not destroy the last valid save.
- To load, read the two files. Discard a file that is too short, has the wrong marker or has a bad checksum. Keep the file with the newer sequence number.
#include "libtini.h"
#define SAVE_MAGIC 0x5A
typedef struct save_data {
uint8_t magic, sequence;
uint32_t best_score;
uint8_t level, checksum; /* checksum is the last field */
} save_data;
static save_data current;
static const char *const slot_name[2] = {"SAVE.A", "SAVE.B"};
static uint8_t newest = 1; /* the slot that holds current; saves use the other slot */
static uint8_t checksum(const save_data *s)
{
const uint8_t *p = (const uint8_t *)s;
uint8_t i, sum = 0xA5;
for (i = 0; i < sizeof(*s) - 1; ++i) sum = (uint8_t)((sum << 1 | sum >> 7) ^ p[i]);
return sum;
}
/* Loads the newer valid slot into current; 0 when no slot is valid. */
uint8_t load_game(void)
{
save_data slot[2];
uint16_t length;
uint8_t i, valid[2];
for (i = 0; i < 2; ++i)
valid[i] = ti_file_read(slot_name[i], &slot[i], sizeof(slot[i]), &length) == TI_OK &&
length == sizeof(slot[i]) && slot[i].magic == SAVE_MAGIC &&
slot[i].checksum == checksum(&slot[i]);
if (!valid[0] && !valid[1]) return 0;
if (valid[0] && valid[1]) i = (int8_t)(slot[1].sequence - slot[0].sequence) > 0;
else i = valid[1];
current = slot[i];
newest = i;
return 1;
}
ti_error save_game(void)
{
ti_error e;
current.magic = SAVE_MAGIC;
++current.sequence;
current.checksum = checksum(¤t);
e = ti_file_write(slot_name[newest ^ 1], ¤t, sizeof(current));
if (e == TI_OK) newest ^= 1;
return e;
}
The sequence comparison is correct when the number goes from 255 to 0. File names use the ProDOS rule: a capital letter, then capitals, digits or periods, with a maximum of 15 characters. A name is relative to the current prefix. This prefix is usually the volume of the boot disk.
What a file call needs
- The file buffer. ProDOS needs the 1 KiB buffer at
_ti_file_buffer, which is $B000-$B3FF in the two shared layouts. If a layout does not define this buffer, as in STARFALL, file calls cannot link. - Memory during a video session. When SHR is on, each call lends AUX bank 0 to ProDOS. It keeps the picture in a temporary resource, which needs one free slot and 32,768 bytes. If these are not available, the call returns
TI_E_NOMEMbefore ProDOS runs. - No other file call. Calls are synchronous. A call made while another call runs returns
TI_E_BUSY.
Time during a save
The audio driver pauses while ProDOS runs. The music becomes silent, effects and speech stop, and the clock stops. When the call returns, the clock starts again without a gap, and the music continues. The picture comes back as it was.
A save needs disk time. Thus save at a natural pause, for example the game-over screen. Expect a short silence.
When a save fails
ti_file_write returns TI_E_IO for a ProDOS failure. ti_file_last_error gives the ProDOS code, for example $2B for a write-protected disk or $48 for a full volume.
Tell the player about the failure in words, and continue the game. Do not change the other slot, because it holds the last valid save.