New save interface.
This commit is contained in:
parent
a0d3cdb876
commit
da8bb41477
21 changed files with 1183 additions and 20 deletions
34
README.md
34
README.md
|
|
@ -26,8 +26,8 @@ make
|
|||
```
|
||||
|
||||
This builds `libjoey.a` for every target whose toolchain is installed,
|
||||
plus the example programs (`hello`, `pattern`, `keys`, `serial`, `joy`,
|
||||
`sprite`, `audio`) for each.
|
||||
plus the example programs (`hello`, `pattern`, `keys`, `serial`, `save`,
|
||||
`joy`, `sprite`, `audio`) for each.
|
||||
|
||||
|
||||
## Building for a single target
|
||||
|
|
@ -432,6 +432,36 @@ layer against a loopback mock with no hardware. Full reference, per-port
|
|||
behavior, and porting notes: [`docs/serial.md`](docs/serial.md).
|
||||
|
||||
|
||||
### Files, saves & disk space (`joey/file.h`) -- opt-in
|
||||
|
||||
File access, not pulled in by `<joey/joey.h>` -- include `<joey/file.h>`. Two
|
||||
directories sit beside the executable: read-only `DATA/` (game content) and
|
||||
writable `SAVES/` (save / preference files, created on demand). Names are
|
||||
relative to their directory (`"hero.sav"`, not `"SAVES/hero.sav"`). Keep save
|
||||
names to 8 characters plus a short extension, letters/digits only, so they fit
|
||||
ProDOS and 8.3. Every call is non-fatal: a failed save reports `false`/`0`.
|
||||
|
||||
```c
|
||||
FILE * jlDataOpen (const char *name, const char *mode); // DATA/ (read-only)
|
||||
|
||||
FILE * jlSaveOpen (const char *name, const char *mode); // SAVES/ stream
|
||||
bool jlSaveWrite (const char *name, const void *buf, uint32_t len);
|
||||
uint32_t jlSaveRead (const char *name, void *buf, uint32_t max);
|
||||
bool jlSaveExists(const char *name);
|
||||
bool jlSaveDelete(const char *name);
|
||||
|
||||
uint32_t jlDiskFree (void); // free bytes on the save volume (0 if unknown)
|
||||
```
|
||||
|
||||
Directory create / delete / free-space use a per-port HAL (POSIX `mkdir` +
|
||||
`remove` + INT 21h on DOS; `mkdir` + GEMDOS `Dfree` on ST; dos.library
|
||||
`CreateDir`/`DeleteFile`/`Info` on Amiga; GS/OS `Create` + a
|
||||
`GetDevNumber`/`DInfo`/`Volume` chain on the IIgs). `scripts/check-save.sh`
|
||||
round-trips the core against real files; `make iigs-verify-save` boots the
|
||||
IIgs save path under MAME. Full reference, per-port behavior, and porting
|
||||
notes: [`docs/save.md`](docs/save.md).
|
||||
|
||||
|
||||
### Debug logging (`joey/debug.h`)
|
||||
|
||||
Crash-tracing logger. Writes are buffered and durable across normal
|
||||
|
|
|
|||
170
examples/save/save.c
Normal file
170
examples/save/save.c
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// Save-file demo: a launch counter that persists across runs, plus a free-disk
|
||||
// bar -- no text assets. On start it reads SAVES/runs.dat, increments the
|
||||
// count, and writes it back; the 16-bit count shows as lit/unlit cells (MSB
|
||||
// left) and a lamp reports whether the save succeeded. A bar underneath is
|
||||
// scaled to jlDiskFree(). SPACE erases the save (count back to 0), ESC quits.
|
||||
//
|
||||
// Run it twice: the count comes back one higher each launch (on the IIgs the
|
||||
// SAVES/ directory is prestaged on the disk image). Files are opt-in -- reached
|
||||
// through <joey/file.h>, which the umbrella <joey/joey.h> does not include.
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <joey/joey.h>
|
||||
#include <joey/file.h>
|
||||
|
||||
#define CELL_W 16
|
||||
#define CELL_H 36
|
||||
#define CELL_GAP 3
|
||||
#define BITS 16
|
||||
#define BITS_ORIGIN_X 12
|
||||
#define BITS_ORIGIN_Y 28
|
||||
|
||||
#define LAMP_X 12
|
||||
#define LAMP_Y 84
|
||||
#define LAMP_W 40
|
||||
#define LAMP_H 20
|
||||
|
||||
#define BAR_X 12
|
||||
#define BAR_Y 128
|
||||
#define BAR_H 18
|
||||
#define BAR_MAX_W 280
|
||||
#define BAR_BYTES_PER_PX 4096u // 1 pixel per 4 KB free
|
||||
|
||||
#define COLOR_BACKGROUND 0
|
||||
#define COLOR_BIT_OFF 1
|
||||
#define COLOR_BIT_ON 2
|
||||
#define COLOR_SAVE_OK 3
|
||||
#define COLOR_SAVE_FAIL 4
|
||||
#define COLOR_BAR_BG 5
|
||||
#define COLOR_BAR_FILL 6
|
||||
|
||||
#define SAVE_NAME "runs.dat"
|
||||
|
||||
static void buildPalette(jlSurfaceT *screen);
|
||||
static void drawBar(jlSurfaceT *screen, uint32_t freeBytes);
|
||||
static void drawBits(jlSurfaceT *screen, uint16_t value);
|
||||
static void drawLamp(jlSurfaceT *screen, bool ok);
|
||||
static uint16_t loadCount(void);
|
||||
static bool storeCount(uint16_t count);
|
||||
|
||||
|
||||
static void buildPalette(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
colors[i] = 0x0000;
|
||||
}
|
||||
colors[COLOR_BACKGROUND] = 0x0000; // black
|
||||
colors[COLOR_BIT_OFF] = 0x0114; // dim blue
|
||||
colors[COLOR_BIT_ON] = 0x00FF; // bright cyan
|
||||
colors[COLOR_SAVE_OK] = 0x00F0; // green
|
||||
colors[COLOR_SAVE_FAIL] = 0x0F00; // red
|
||||
colors[COLOR_BAR_BG] = 0x0222; // gray
|
||||
colors[COLOR_BAR_FILL] = 0x00FA; // teal
|
||||
|
||||
jlPaletteSet(screen, 0, colors);
|
||||
}
|
||||
|
||||
|
||||
static void drawBar(jlSurfaceT *screen, uint32_t freeBytes) {
|
||||
uint32_t px;
|
||||
|
||||
jlFillRect(screen, BAR_X, BAR_Y, BAR_MAX_W, BAR_H, COLOR_BAR_BG);
|
||||
px = freeBytes / BAR_BYTES_PER_PX;
|
||||
if (px > (uint32_t)BAR_MAX_W) {
|
||||
px = (uint32_t)BAR_MAX_W;
|
||||
}
|
||||
if (px > 0u) {
|
||||
jlFillRect(screen, BAR_X, BAR_Y, (int16_t)px, BAR_H, COLOR_BAR_FILL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void drawBits(jlSurfaceT *screen, uint16_t value) {
|
||||
int16_t i;
|
||||
|
||||
for (i = 0; i < BITS; i++) {
|
||||
int16_t x = (int16_t)(BITS_ORIGIN_X + i * (CELL_W + CELL_GAP));
|
||||
bool set = (value & (0x8000u >> i)) != 0u;
|
||||
|
||||
jlFillRect(screen, x, BITS_ORIGIN_Y, CELL_W, CELL_H, set ? COLOR_BIT_ON : COLOR_BIT_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void drawLamp(jlSurfaceT *screen, bool ok) {
|
||||
jlFillRect(screen, LAMP_X, LAMP_Y, LAMP_W, LAMP_H, ok ? COLOR_SAVE_OK : COLOR_SAVE_FAIL);
|
||||
}
|
||||
|
||||
|
||||
static uint16_t loadCount(void) {
|
||||
uint16_t count;
|
||||
|
||||
count = 0u;
|
||||
// A short read (missing file) leaves count at 0 -- first launch.
|
||||
jlSaveRead(SAVE_NAME, &count, (uint32_t)sizeof(count));
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
static bool storeCount(uint16_t count) {
|
||||
return jlSaveWrite(SAVE_NAME, &count, (uint32_t)sizeof(count));
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
uint16_t count;
|
||||
bool saved;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = jlStageGet();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "jlStageGet returned NULL\n");
|
||||
jlShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Persist: read the prior count, bump it, write it back.
|
||||
count = (uint16_t)(loadCount() + 1u);
|
||||
saved = storeCount(count);
|
||||
|
||||
buildPalette(screen);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlSurfaceClear(screen, COLOR_BACKGROUND);
|
||||
drawBits(screen, count);
|
||||
drawLamp(screen, saved);
|
||||
drawBar(screen, jlDiskFree());
|
||||
jlStagePresent();
|
||||
jlInputPoll();
|
||||
|
||||
for (;;) {
|
||||
jlInputPoll();
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
if (jlKeyPressed(KEY_SPACE)) {
|
||||
// Erase the save and reset the counter to zero.
|
||||
jlSaveDelete(SAVE_NAME);
|
||||
count = 0u;
|
||||
saved = storeCount(count);
|
||||
drawBits(screen, count);
|
||||
drawLamp(screen, saved);
|
||||
drawBar(screen, jlDiskFree());
|
||||
jlStagePresent();
|
||||
}
|
||||
}
|
||||
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,21 +1,66 @@
|
|||
// JoeyLib data-file access.
|
||||
// JoeyLib file access: read-only game data, writable save files, and a disk-
|
||||
// space query.
|
||||
//
|
||||
// Every game's runtime data lives under a single DATA/ directory that
|
||||
// sits next to the executable (staged there by the per-platform disk /
|
||||
// directory packagers). jlDataOpen forces that prefix so callers pass a
|
||||
// bare name relative to DATA/ and never hard-code the prefix themselves --
|
||||
// e.g. jlDataOpen("levels/title.dat", "rb") opens DATA/levels/title.dat.
|
||||
// Two directories sit next to the executable, staged there by the per-platform
|
||||
// disk / directory packagers:
|
||||
// DATA/ -- read-only game content (levels, sprites, audio). jlDataOpen.
|
||||
// SAVES/ -- writable save / preference files. jlSave*.
|
||||
// Both are addressed by a bare name relative to their directory; the prefix is
|
||||
// forced here so callers never hard-code "DATA/" or "SAVES/".
|
||||
|
||||
#ifndef JOEYLIB_FILE_H
|
||||
#define JOEYLIB_FILE_H
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
// Open a data file by its name relative to the DATA/ directory. The
|
||||
// DATA/ prefix is prepended automatically; pass "levels/title.dat", not
|
||||
// "DATA/levels/title.dat". mode is a standard fopen mode string. Returns
|
||||
// NULL if the resulting path would overflow the internal buffer or the
|
||||
// file cannot be opened.
|
||||
#include "types.h"
|
||||
|
||||
// ----- Read-only game data (DATA/) -----
|
||||
|
||||
// Open a data file by its name relative to the DATA/ directory. The DATA/
|
||||
// prefix is prepended automatically; pass "levels/title.dat", not
|
||||
// "DATA/levels/title.dat". mode is a standard fopen mode string. Returns NULL
|
||||
// if the resulting path would overflow the internal buffer or the file cannot
|
||||
// be opened.
|
||||
FILE *jlDataOpen(const char *name, const char *mode);
|
||||
|
||||
// ----- Writable save files (SAVES/) -----
|
||||
//
|
||||
// Save / preference data lives under a SAVES/ directory beside the executable
|
||||
// -- a writable sibling of the read-only DATA/ tree. Names are relative to
|
||||
// SAVES/: pass "hero.sav", not "SAVES/hero.sav". Keep names portable across
|
||||
// the retro filesystems: at most 8 characters plus a short extension, letters
|
||||
// and digits only, so they fit ProDOS (15-char) and DOS 8.3.
|
||||
//
|
||||
// A failed open / write is non-fatal and simply reports failure (false / 0),
|
||||
// so save code can degrade gracefully on a platform or volume where SAVES/ is
|
||||
// unavailable.
|
||||
|
||||
// Open a save file for streaming, name relative to SAVES/. mode is a standard
|
||||
// fopen mode string; for write / append modes the SAVES/ directory is created
|
||||
// on demand first. Returns NULL on failure. Use this for large or incremental
|
||||
// saves; for a whole-file blob prefer jlSaveWrite / jlSaveRead.
|
||||
FILE *jlSaveOpen(const char *name, const char *mode);
|
||||
|
||||
// Write `len` bytes from `buf` to save file `name`, replacing any existing
|
||||
// contents. Returns true only if all `len` bytes were written.
|
||||
bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
|
||||
|
||||
// Read up to `max` bytes from save file `name` into `buf`. Returns the number
|
||||
// of bytes read (0 if the file is missing or empty).
|
||||
uint32_t jlSaveRead(const char *name, void *buf, uint32_t max);
|
||||
|
||||
// True if save file `name` exists and can be opened for reading.
|
||||
bool jlSaveExists(const char *name);
|
||||
|
||||
// Delete save file `name`. Returns true if the file was removed.
|
||||
bool jlSaveDelete(const char *name);
|
||||
|
||||
// ----- Disk space -----
|
||||
|
||||
// Free space, in bytes, on the volume that holds SAVES/ (the program's
|
||||
// volume). Returns 0 if it cannot be determined (e.g. the port has no query
|
||||
// for it). Saturates at UINT32_MAX (4 GB - 1) on larger volumes.
|
||||
uint32_t jlDiskFree(void);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -144,6 +144,11 @@
|
|||
#define JL_HAS_SERIAL_WRITE
|
||||
#define JL_HAS_SERIAL_AVAILABLE
|
||||
#define JL_HAS_SERIAL_FLUSH
|
||||
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
|
||||
// $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0).
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
// millisElapsed: generic frameCount*1000/frameHz (no override)
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
#define JL_HAS_SURFACE_CLEAR // planar clear, function
|
||||
|
|
@ -208,6 +213,10 @@
|
|||
#define JL_HAS_SERIAL_WRITE
|
||||
#define JL_HAS_SERIAL_AVAILABLE
|
||||
#define JL_HAS_SERIAL_FLUSH
|
||||
// save files + disk space (CreateDir/DeleteFile/Info via dos.library)
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
// millisElapsed: generic frameCount*1000/frameHz (no override)
|
||||
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
||||
#define JL_HAS_SURFACE_CLEAR // planar clear, function
|
||||
|
|
@ -273,6 +282,10 @@
|
|||
#define JL_HAS_SERIAL_WRITE
|
||||
#define JL_HAS_SERIAL_AVAILABLE
|
||||
#define JL_HAS_SERIAL_FLUSH
|
||||
// save files + disk space (POSIX mkdir/remove + GEMDOS Dfree)
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
#elif defined(JOEYLIB_PLATFORM_DOS)
|
||||
// chunky generics: surface clear, draw pixel/line/circle, fill circle/rect,
|
||||
// tiles, surface readers, allocation -- DOS overrides the timing services
|
||||
|
|
@ -317,6 +330,10 @@
|
|||
#define JL_HAS_SERIAL_WRITE
|
||||
#define JL_HAS_SERIAL_AVAILABLE
|
||||
#define JL_HAS_SERIAL_FLUSH
|
||||
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free)
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
#elif defined(JOEYLIB_PLATFORM_BLANK)
|
||||
// Copy-to-start template. It overrides ONLY the platform SERVICES (TODO
|
||||
// stubs in src/blank/blank.c). Everything graphical -- draw / tile / sprite
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ KEYS_SRC := $(EXAMPLES)/keys/keys.c
|
|||
KEYS_BIN := $(BINDIR)/Keys
|
||||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||
SERIAL_BIN := $(BINDIR)/Serial
|
||||
SAVE_SRC := $(EXAMPLES)/save/save.c
|
||||
SAVE_BIN := $(BINDIR)/Save
|
||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||
JOY_BIN := $(BINDIR)/Joy
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
|
|
@ -135,7 +137,7 @@ DATA_DIR := $(BINDIR)/DATA
|
|||
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
|
||||
|
||||
.PHONY: all amiga clean-amiga
|
||||
all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -199,6 +201,10 @@ $(SERIAL_BIN): $(SERIAL_SRC) $(LIB)
|
|||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
|
||||
$(SAVE_BIN): $(SAVE_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ KEYS_SRC := $(EXAMPLES)/keys/keys.c
|
|||
KEYS_BIN := $(BINDIR)/KEYS.PRG
|
||||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||
SERIAL_BIN := $(BINDIR)/SERIAL.PRG
|
||||
SAVE_SRC := $(EXAMPLES)/save/save.c
|
||||
SAVE_BIN := $(BINDIR)/SAVE.PRG
|
||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||
JOY_BIN := $(BINDIR)/JOY.PRG
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
|
|
@ -115,7 +117,7 @@ DATA_DIR := $(BINDIR)/DATA
|
|||
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
|
||||
|
||||
.PHONY: all atarist clean-atarist
|
||||
all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -186,6 +188,10 @@ $(SERIAL_BIN): $(SERIAL_SRC) $(LIB)
|
|||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(SAVE_BIN): $(SAVE_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ KEYS_SRC := $(EXAMPLES)/keys/keys.c
|
|||
KEYS_BIN := $(BINDIR)/KEYS.EXE
|
||||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||
SERIAL_BIN := $(BINDIR)/SERIAL.EXE
|
||||
SAVE_SRC := $(EXAMPLES)/save/save.c
|
||||
SAVE_BIN := $(BINDIR)/SAVE.EXE
|
||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||
JOY_BIN := $(BINDIR)/JOY.EXE
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
|
|
@ -130,7 +132,7 @@ STAXI_ASSET_DSTS += $(STAXI_SPC_RUN)
|
|||
MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel
|
||||
|
||||
.PHONY: all dos clean-dos
|
||||
all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -189,6 +191,11 @@ $(SERIAL_BIN): $(SERIAL_SRC) $(LIB)
|
|||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(SAVE_BIN): $(SAVE_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
|
|
|
|||
18
make/iigs.mk
18
make/iigs.mk
|
|
@ -47,6 +47,7 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
|||
DRAW_SRC := $(EXAMPLES)/draw/draw.c
|
||||
KEYS_SRC := $(EXAMPLES)/keys/keys.c
|
||||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||
SAVE_SRC := $(EXAMPLES)/save/save.c
|
||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
UBER_SRC := $(EXAMPLES)/uber/uber.c
|
||||
|
|
@ -66,7 +67,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin
|
|||
NTP_ASM := $(BUILD)/audio/ntpdata.s
|
||||
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
|
||||
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify clean-iigs
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save clean-iigs
|
||||
|
||||
# Default: compile-check the library + run the end-to-end smoke test.
|
||||
all iigs: iigs-lib iigs-clang-smoke
|
||||
|
|
@ -78,7 +79,7 @@ all iigs: iigs-lib iigs-clang-smoke
|
|||
# NTP replayer from disk (see the $(BINDIR)/AUDIO rule). Leaving them
|
||||
# out made `make iigs` silently test 8-day-old STAXI binaries against
|
||||
# new assets (the 2026-07-20 gear-out/no-flame reports).
|
||||
iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL $(BINDIR)/JOY \
|
||||
iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL $(BINDIR)/SAVE $(BINDIR)/JOY \
|
||||
$(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \
|
||||
$(BINDIR)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO
|
||||
|
||||
|
|
@ -113,6 +114,15 @@ iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC)
|
|||
iigs-verify: iigs-disk
|
||||
$(REPO_DIR)/scripts/verify-iigs.sh $(or $(VERIFY_EXAMPLE),draw)
|
||||
|
||||
# Boot-verify every launchable example on joey.2mg (per-example thresholds).
|
||||
iigs-verify-all: iigs-disk
|
||||
$(REPO_DIR)/scripts/verify-iigs-all.sh
|
||||
|
||||
# Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/
|
||||
# Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar.
|
||||
iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW
|
||||
$(REPO_DIR)/scripts/verify-iigs-save.sh
|
||||
|
||||
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
|
||||
@mkdir -p $(BUILD)/audio
|
||||
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
|
||||
|
|
@ -164,6 +174,10 @@ $(BINDIR)/SERIAL: $(SERIAL_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
|||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SERIAL.d $(INCLUDES) -o $@ $(SERIAL_SRC) $(LIB_SRCS)
|
||||
|
||||
$(BINDIR)/SAVE: $(SAVE_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SAVE.d $(INCLUDES) -o $@ $(SAVE_SRC) $(LIB_SRCS)
|
||||
|
||||
$(BINDIR)/JOY: $(JOY_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
|
||||
|
|
|
|||
25
scripts/check-save.sh
Executable file
25
scripts/check-save.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env bash
|
||||
# check-save.sh - Host-cc harness for the save-file core (src/core/save.c).
|
||||
# Compiles it with a POSIX save HAL (tests/host/saveHost.c) and runs real
|
||||
# save/read/exists/delete/disk-free round-trips inside a throwaway temp cwd, so
|
||||
# the SAVES/ directory it creates lands in that temp dir and is cleaned up.
|
||||
#
|
||||
# The BLANK platform block registers no JL_HAS_SAVE_*, so the save op flags are
|
||||
# passed on the command line to route jlpSave*/jlpDiskFree to the host HAL
|
||||
# instead of the generic no-op. Exits nonzero on any failed check (CI gate).
|
||||
set -euo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
cc=${CC:-cc}
|
||||
work=$(mktemp -d -t joeylib-save.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
"$cc" -DJOEYLIB_PLATFORM_BLANK \
|
||||
-DJL_HAS_SAVE_DIR_ENSURE -DJL_HAS_SAVE_DELETE -DJL_HAS_DISK_FREE \
|
||||
-I"$repo/include" -I"$repo/include/joey" -I"$repo/src/core" -Wall -Wextra \
|
||||
"$repo/src/core/save.c" "$repo/tests/host/saveHost.c" \
|
||||
-o "$work/saveHost"
|
||||
|
||||
# Run from the temp dir so the SAVES/ tree it creates is disposable.
|
||||
cd "$work"
|
||||
"$work/saveHost"
|
||||
|
|
@ -32,6 +32,15 @@ trap 'rm -rf "$work"' EXIT
|
|||
rm -f "$OUT"
|
||||
"$CADIUS" CREATEVOLUME "$OUT" "$VOL" 800KB >/dev/null
|
||||
|
||||
# Writable save area for the save-file API (joey/file.h -> jlSave*). GS/OS's
|
||||
# libc has no mkdir, but the IIgs save HAL creates SAVES/ at runtime via GS/OS
|
||||
# Create; prestaging it here as well means saves work even before that call and
|
||||
# on a first run. Set JOEY_SKIP_SAVES_PRESTAGE=1 to omit it (tests runtime
|
||||
# creation in isolation).
|
||||
if [ -z "${JOEY_SKIP_SAVES_PRESTAGE:-}" ]; then
|
||||
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/SAVES" >/dev/null
|
||||
fi
|
||||
|
||||
freeBlocks() {
|
||||
"$CADIUS" CATALOG "$OUT" 2>/dev/null \
|
||||
| grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1
|
||||
|
|
|
|||
39
scripts/verify-iigs-all.sh
Executable file
39
scripts/verify-iigs-all.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
# verify-iigs-all.sh - Boot-verify EVERY launchable example on the built
|
||||
# joey.2mg (the S16 apps cadius packed onto the volume), each via verify-iigs.sh
|
||||
# with its per-example distinct-color threshold. Reports one line per example
|
||||
# and exits nonzero if any example fails to render.
|
||||
#
|
||||
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built
|
||||
# (`make iigs-disk`); apple2gs ROM + gsos-system.po (see verify-iigs.sh).
|
||||
set -uo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
source "$repo/toolchains/env.sh" 2>/dev/null || true
|
||||
: "${LLVM816_ROOT:?source toolchains/env.sh first}"
|
||||
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
|
||||
disk="$repo/build/iigs/bin/joey.2mg"
|
||||
|
||||
[ -f "$disk" ] || { echo "verify-iigs-all: $disk missing (run 'make iigs-disk')" >&2; exit 2; }
|
||||
[ -x "$CADIUS" ] || { echo "verify-iigs-all: cadius not found at $CADIUS" >&2; exit 2; }
|
||||
|
||||
# Launchable examples = ProDOS type S16 files in the catalog, lowercased.
|
||||
apps=$("$CADIUS" CATALOG "$disk" 2>/dev/null | awk '$2 == "S16" { print tolower($1) }' | sort)
|
||||
[ -n "$apps" ] || { echo "verify-iigs-all: no S16 apps found on $disk" >&2; exit 2; }
|
||||
|
||||
echo "verify-iigs-all: booting $(echo "$apps" | wc -w) examples off joey.2mg"
|
||||
fails=0
|
||||
for ex in $apps; do
|
||||
out=$("$repo/scripts/verify-iigs.sh" "$ex" 2>&1)
|
||||
rc=$?
|
||||
info=$(echo "$out" | grep -oE 'distinctNibbles=[0-9]+ nonZeroBytes=[0-9]+' | tail -1)
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo " PASS $ex ($info)"
|
||||
else
|
||||
echo " FAIL $ex ($info)"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "verify-iigs-all: $fails failed"
|
||||
[ "$fails" -eq 0 ]
|
||||
119
scripts/verify-iigs-save.sh
Executable file
119
scripts/verify-iigs-save.sh
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env bash
|
||||
# verify-iigs-save.sh - Runtime gate for the IIgs save-file HAL's GS/OS calls
|
||||
# (Create $2001 for the SAVES/ directory, Volume $2008 for jlDiskFree). Boots
|
||||
# GS/OS under MAME, launches the SAVE example off a freshly built JOEYLIB disk,
|
||||
# and reads two spots out of the Super Hi-Res framebuffer:
|
||||
# - the save lamp at (32,94): color 3 (COLOR_SAVE_OK) means jlSaveWrite
|
||||
# succeeded, i.e. jlpSaveDirEnsure (GS/OS Create) + the write into SAVES/
|
||||
# both worked; color 4 means it failed.
|
||||
# - the disk-free bar row at y=137: any COLOR_BAR_FILL (6) pixels mean
|
||||
# jlDiskFree() (GS/OS Volume) returned a nonzero free-byte count.
|
||||
# PASS requires lamp==3 AND barFill>0.
|
||||
#
|
||||
# Set JOEY_SAVE_NOPRESTAGE=1 to build the disk WITHOUT prestaging SAVES/, which
|
||||
# isolates GS/OS Create: a green lamp then proves Create made the directory
|
||||
# from scratch (not that a prestaged one was merely present).
|
||||
#
|
||||
# Requires: toolchains/env.sh sourced; build/iigs/bin/SAVE + DRAW built; the
|
||||
# apple2gs ROM and gsos-system.po present (same as verify-iigs.sh).
|
||||
set -euo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
source "$repo/toolchains/env.sh" 2>/dev/null || true
|
||||
: "${LLVM816_ROOT:?source toolchains/env.sh first}"
|
||||
|
||||
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
|
||||
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
|
||||
readFrame="${MAME_READ_FRAME:-9000}"
|
||||
|
||||
[ -f "$repo/build/iigs/bin/SAVE" ] || { echo "verify-iigs-save: build/iigs/bin/SAVE missing (make iigs first)" >&2; exit 2; }
|
||||
[ -f "$sys_disk" ] || { echo "verify-iigs-save: missing $sys_disk" >&2; exit 2; }
|
||||
|
||||
work=$(mktemp -d -t joeylib-vsave.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# Build the data disk with SAVE (DRAW keeps a known-good sibling on the volume).
|
||||
# JOEY_SAVE_NOPRESTAGE=1 omits the prestaged SAVES/, isolating GS/OS Create.
|
||||
prestage_env=""
|
||||
if [ -n "${JOEY_SAVE_NOPRESTAGE:-}" ]; then
|
||||
prestage_env="JOEY_SKIP_SAVES_PRESTAGE=1"
|
||||
echo "verify-iigs-save: building WITHOUT prestaged SAVES/ (isolating GS/OS Create)"
|
||||
fi
|
||||
env $prestage_env JOEY_DISK_EXAMPLES="SAVE DRAW" "$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" >/dev/null
|
||||
cp "$sys_disk" "$work/boot.po"
|
||||
|
||||
cat > "$work/vsave.lua" <<LUA
|
||||
local cpu = manager.machine.devices[":maincpu"]
|
||||
local mem = cpu.spaces["program"]
|
||||
local nat = manager.machine.natkeyboard
|
||||
local frame = 0
|
||||
local idx = 1
|
||||
|
||||
local function field(port, name)
|
||||
local p = manager.machine.ioport.ports[port]
|
||||
if p == nil then return nil end
|
||||
return p.fields[name]
|
||||
end
|
||||
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
|
||||
local function press(f) if f then f:set_value(1) end end
|
||||
local function release(f) if f then f:set_value(0) end end
|
||||
|
||||
local function nib(x, y)
|
||||
local b = mem:read_u8(0xE12000 + y * 160 + (x >> 1))
|
||||
if (x & 1) == 0 then return (b >> 4) & 0x0F else return b & 0x0F end
|
||||
end
|
||||
|
||||
local function report()
|
||||
local lamp = nib(32, 94) -- COLOR_SAVE_OK=3 / COLOR_SAVE_FAIL=4
|
||||
local barFill = 0
|
||||
for x = 12, 291 do -- bar row; COLOR_BAR_FILL=6
|
||||
if nib(x, 137) == 6 then barFill = barFill + 1 end
|
||||
end
|
||||
io.write(string.format("VERIFY-SAVE lamp=%d barFill=%d\n", lamp, barFill))
|
||||
io.flush()
|
||||
end
|
||||
|
||||
local steps = {
|
||||
{3000, function() nat:post("J") end},
|
||||
{3120, function() press(key_cmd) end},
|
||||
{3126, function() nat:post("o") end},
|
||||
{3180, function() release(key_cmd) end},
|
||||
{3540, function() nat:post("SAVE") end},
|
||||
{3660, function() press(key_cmd) end},
|
||||
{3666, function() nat:post("o") end},
|
||||
{3720, function() release(key_cmd) end},
|
||||
{$readFrame, function() report(); manager.machine:exit() end},
|
||||
}
|
||||
|
||||
emu.register_frame_done(function()
|
||||
frame = frame + 1
|
||||
while idx <= #steps and frame >= steps[idx][1] do
|
||||
steps[idx][2]()
|
||||
idx = idx + 1
|
||||
end
|
||||
end)
|
||||
LUA
|
||||
|
||||
cd "$work"
|
||||
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
|
||||
timeout 300 mame apple2gs \
|
||||
-rompath "$rompath" \
|
||||
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
|
||||
-video none -sound none -nothrottle \
|
||||
-autoboot_script "$work/vsave.lua" </dev/null 2>&1) || true
|
||||
|
||||
line=$(echo "$out" | grep -E '^VERIFY-SAVE ' | tail -1)
|
||||
echo "$line"
|
||||
if [ -z "$line" ]; then
|
||||
echo "verify-iigs-save: FAIL - no report (boot/launch failed)" >&2
|
||||
echo "$out" | tail -15 >&2
|
||||
exit 1
|
||||
fi
|
||||
lamp=$(echo "$line" | sed -E 's/.*lamp=([0-9]+).*/\1/')
|
||||
barFill=$(echo "$line" | sed -E 's/.*barFill=([0-9]+).*/\1/')
|
||||
if [ "$lamp" = "3" ] && [ "$barFill" -gt 0 ]; then
|
||||
echo "verify-iigs-save: PASS (save OK lamp + disk-free bar; GS/OS Create+Volume work)"
|
||||
else
|
||||
echo "verify-iigs-save: FAIL (lamp=$lamp expected 3, barFill=$barFill expected >0)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -14,15 +14,27 @@
|
|||
# Usage: scripts/verify-iigs.sh [example] [minDistinct]
|
||||
# example lowercase name (default: draw). Must be on joey.2mg (run
|
||||
# `make iigs-disk` first). The volume is JOEYLIB.
|
||||
# minDistinct PASS threshold for distinct nibble values (default: 4).
|
||||
# minDistinct PASS threshold for distinct nibble values. If omitted, a
|
||||
# per-example default is used: 4 for most demos, but 3 for the
|
||||
# deliberately sparse ones (keys draws a mostly-monochrome grid;
|
||||
# sprite draws one small ball on a black background), which
|
||||
# legitimately render only ~3 distinct colors. An explicit
|
||||
# second arg always overrides.
|
||||
#
|
||||
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built.
|
||||
set -euo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
example=${1:-draw}
|
||||
minDistinct=${2:-4}
|
||||
NAME=${example^^}
|
||||
if [ -n "${2:-}" ]; then
|
||||
minDistinct=$2
|
||||
else
|
||||
case "$example" in
|
||||
keys|sprite) minDistinct=3 ;;
|
||||
*) minDistinct=4 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
|
||||
data_disk=$repo/build/iigs/bin/joey.2mg
|
||||
|
|
|
|||
67
src/amiga/save.c
Normal file
67
src/amiga/save.c
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Amiga save-file + disk-space HAL.
|
||||
//
|
||||
// SAVES/ is a real directory next to the executable, made with dos.library
|
||||
// CreateDir; delete is DeleteFile; free space is Info() on a lock to PROGDIR:
|
||||
// (the program's -- and thus SAVES/'s -- volume). All ROM-backed, so no libc
|
||||
// filesystem dependency.
|
||||
|
||||
#include <exec/types.h>
|
||||
#include <dos/dos.h>
|
||||
|
||||
#include <proto/dos.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
#define AMIGA_MAX_U32 0xFFFFFFFFu
|
||||
|
||||
|
||||
uint32_t jlpDiskFree(void) {
|
||||
struct InfoData info;
|
||||
BPTR lock;
|
||||
uint32_t freeBlocks;
|
||||
uint32_t bytesPerBlock;
|
||||
|
||||
lock = Lock((CONST_STRPTR)"PROGDIR:", SHARED_LOCK);
|
||||
if (lock == 0) {
|
||||
return 0u;
|
||||
}
|
||||
if (Info(lock, &info) == 0) {
|
||||
UnLock(lock);
|
||||
return 0u;
|
||||
}
|
||||
UnLock(lock);
|
||||
|
||||
freeBlocks = (info.id_NumBlocks > info.id_NumBlocksUsed)
|
||||
? (uint32_t)(info.id_NumBlocks - info.id_NumBlocksUsed) : 0u;
|
||||
bytesPerBlock = (uint32_t)info.id_BytesPerBlock;
|
||||
// Saturate in 32-bit (no 64-bit multiply helper on the 68000).
|
||||
if (bytesPerBlock != 0u && freeBlocks > AMIGA_MAX_U32 / bytesPerBlock) {
|
||||
return AMIGA_MAX_U32;
|
||||
}
|
||||
return freeBlocks * bytesPerBlock;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDelete(const char *path) {
|
||||
return DeleteFile((CONST_STRPTR)path) != 0;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDirEnsure(const char *dir) {
|
||||
BPTR lock;
|
||||
|
||||
lock = CreateDir((CONST_STRPTR)dir);
|
||||
if (lock != 0) {
|
||||
UnLock(lock);
|
||||
return true;
|
||||
}
|
||||
// CreateDir fails if the directory already exists -- confirm via Lock.
|
||||
lock = Lock((CONST_STRPTR)dir, SHARED_LOCK);
|
||||
if (lock != 0) {
|
||||
UnLock(lock);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
50
src/atarist/save.c
Normal file
50
src/atarist/save.c
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Atari ST save-file + disk-space HAL.
|
||||
//
|
||||
// SAVES/ is a real GEMDOS subdirectory next to the .PRG, made with POSIX
|
||||
// mkdir; delete is stdio remove(); free space comes from GEMDOS Dfree on the
|
||||
// default drive.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <mint/osbind.h>
|
||||
#include <mint/ostruct.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
#define SAVE_DIR_MODE 0777
|
||||
#define ST_MAX_U32 0xFFFFFFFFu
|
||||
|
||||
|
||||
uint32_t jlpDiskFree(void) {
|
||||
_DISKINFO info;
|
||||
uint32_t bytesPerClus;
|
||||
|
||||
// Dfree drive 0 = the current (default) GEMDOS drive; < 0 is an error.
|
||||
if (Dfree(&info, 0) < 0) {
|
||||
return 0u;
|
||||
}
|
||||
// Saturate in 32-bit (no 64-bit multiply helper on the 68000).
|
||||
bytesPerClus = (uint32_t)info.b_secsiz * (uint32_t)info.b_clsiz;
|
||||
if (bytesPerClus != 0u && (uint32_t)info.b_free > ST_MAX_U32 / bytesPerClus) {
|
||||
return ST_MAX_U32;
|
||||
}
|
||||
return (uint32_t)info.b_free * bytesPerClus;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDelete(const char *path) {
|
||||
return remove(path) == 0;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDirEnsure(const char *dir) {
|
||||
struct stat st;
|
||||
|
||||
if (mkdir(dir, SAVE_DIR_MODE) == 0) {
|
||||
return true;
|
||||
}
|
||||
return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
|
@ -995,6 +995,35 @@ void jlpGenericSerialFlush(void);
|
|||
#endif
|
||||
|
||||
|
||||
// --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) ---
|
||||
// Function overrides (not IIgs asm macros), so no entry in the macro-consistency
|
||||
// #error block above. Core layer is src/core/save.c; publics in joey/file.h.
|
||||
bool jlpGenericSaveDirEnsure(const char *dir);
|
||||
#if !defined(jlpSaveDirEnsure)
|
||||
#if defined(JL_HAS_SAVE_DIR_ENSURE)
|
||||
bool jlpSaveDirEnsure(const char *dir);
|
||||
#else
|
||||
#define jlpSaveDirEnsure(_d) jlpGenericSaveDirEnsure((_d))
|
||||
#endif
|
||||
#endif
|
||||
bool jlpGenericSaveDelete(const char *path);
|
||||
#if !defined(jlpSaveDelete)
|
||||
#if defined(JL_HAS_SAVE_DELETE)
|
||||
bool jlpSaveDelete(const char *path);
|
||||
#else
|
||||
#define jlpSaveDelete(_p) jlpGenericSaveDelete((_p))
|
||||
#endif
|
||||
#endif
|
||||
uint32_t jlpGenericDiskFree(void);
|
||||
#if !defined(jlpDiskFree)
|
||||
#if defined(JL_HAS_DISK_FREE)
|
||||
uint32_t jlpDiskFree(void);
|
||||
#else
|
||||
#define jlpDiskFree() jlpGenericDiskFree()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
// --- Large allocation (malloc/free generic default; IIgs Memory Manager override) ---
|
||||
void *jlpGenericBigAlloc(uint32_t bytes);
|
||||
void jlpGenericBigFree(void *p);
|
||||
|
|
|
|||
124
src/core/save.c
Normal file
124
src/core/save.c
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Writable save files (SAVES/) and a disk-space query.
|
||||
//
|
||||
// Mirrors jlDataOpen's forced-prefix approach (see assetLoad.c) but rooted at
|
||||
// the writable SAVES/ tree instead of read-only DATA/, and it creates SAVES/
|
||||
// on demand before a write open. Blob read/write/exists are plain stdio over
|
||||
// jlSaveOpen; only the two operations stdio cannot do portably -- create the
|
||||
// directory and delete a file -- plus the disk-free query are per-port HAL
|
||||
// hooks (jlpSaveDirEnsure / jlpSaveDelete / jlpDiskFree).
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
#define SAVE_DIR "SAVES"
|
||||
#define SAVE_DIR_PREFIX "SAVES/"
|
||||
#define SAVE_PATH_MAX 256
|
||||
|
||||
|
||||
// ----- Helpers -----
|
||||
|
||||
static bool buildSavePath(char *out, size_t cap, const char *name);
|
||||
|
||||
|
||||
// Join SAVE_DIR_PREFIX + name into `out`. Returns false if it would overflow.
|
||||
static bool buildSavePath(char *out, size_t cap, const char *name) {
|
||||
int written;
|
||||
|
||||
written = snprintf(out, cap, SAVE_DIR_PREFIX "%s", name);
|
||||
return written >= 0 && (size_t)written < cap;
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
uint32_t jlDiskFree(void) {
|
||||
return jlpDiskFree();
|
||||
}
|
||||
|
||||
|
||||
bool jlSaveDelete(const char *name) {
|
||||
char path[SAVE_PATH_MAX];
|
||||
|
||||
if (name == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (!buildSavePath(path, sizeof(path), name)) {
|
||||
return false;
|
||||
}
|
||||
return jlpSaveDelete(path);
|
||||
}
|
||||
|
||||
|
||||
bool jlSaveExists(const char *name) {
|
||||
FILE *fp;
|
||||
|
||||
if (name == NULL) {
|
||||
return false;
|
||||
}
|
||||
fp = jlSaveOpen(name, "rb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
FILE *jlSaveOpen(const char *name, const char *mode) {
|
||||
char path[SAVE_PATH_MAX];
|
||||
|
||||
if (name == NULL || mode == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
// Create SAVES/ before a write / append open. The result is advisory: if
|
||||
// the directory cannot be made, fopen below simply fails and the caller
|
||||
// sees NULL.
|
||||
if (strchr(mode, 'w') != NULL || strchr(mode, 'a') != NULL) {
|
||||
(void)jlpSaveDirEnsure(SAVE_DIR);
|
||||
}
|
||||
if (!buildSavePath(path, sizeof(path), name)) {
|
||||
return NULL;
|
||||
}
|
||||
return fopen(path, mode);
|
||||
}
|
||||
|
||||
|
||||
uint32_t jlSaveRead(const char *name, void *buf, uint32_t max) {
|
||||
FILE *fp;
|
||||
size_t got;
|
||||
|
||||
if (name == NULL || buf == NULL || max == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
fp = jlSaveOpen(name, "rb");
|
||||
if (fp == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
got = fread(buf, 1u, (size_t)max, fp);
|
||||
fclose(fp);
|
||||
return (uint32_t)got;
|
||||
}
|
||||
|
||||
|
||||
bool jlSaveWrite(const char *name, const void *buf, uint32_t len) {
|
||||
FILE *fp;
|
||||
size_t written;
|
||||
|
||||
if (name == NULL) {
|
||||
return false;
|
||||
}
|
||||
fp = jlSaveOpen(name, "wb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
written = 0u;
|
||||
if (buf != NULL && len > 0u) {
|
||||
written = fwrite(buf, 1u, (size_t)len, fp);
|
||||
}
|
||||
fclose(fp);
|
||||
return written == (size_t)len;
|
||||
}
|
||||
53
src/dos/save.c
Normal file
53
src/dos/save.c
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// DOS save-file + disk-space HAL.
|
||||
//
|
||||
// SAVES/ is a real subdirectory next to the .EXE, made with POSIX mkdir;
|
||||
// delete is stdio remove(); free space comes from DOS INT 21h AH=36h (Get
|
||||
// Disk Free Space) on the default drive.
|
||||
|
||||
#include <dpmi.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
// FAT ignores mode bits; 0777 is the conventional "create it" request.
|
||||
#define SAVE_DIR_MODE 0777
|
||||
#define DOS_MAX_U32 0xFFFFFFFFu
|
||||
|
||||
|
||||
uint32_t jlpDiskFree(void) {
|
||||
__dpmi_regs r;
|
||||
uint32_t bytesPerClus;
|
||||
|
||||
r.h.ah = 0x36u; // Get Disk Free Space
|
||||
r.h.dl = 0x00u; // 0 = default drive
|
||||
__dpmi_int(0x21, &r);
|
||||
if (r.x.ax == 0xFFFFu) {
|
||||
return 0u; // invalid drive
|
||||
}
|
||||
// AX = sectors/cluster, CX = bytes/sector, BX = free clusters. Saturate
|
||||
// in 32-bit rather than pull a 64-bit multiply helper onto the target.
|
||||
bytesPerClus = (uint32_t)r.x.ax * (uint32_t)r.x.cx; // <= 65535*65535 < 2^32
|
||||
if (bytesPerClus != 0u && (uint32_t)r.x.bx > DOS_MAX_U32 / bytesPerClus) {
|
||||
return DOS_MAX_U32;
|
||||
}
|
||||
return bytesPerClus * (uint32_t)r.x.bx;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDelete(const char *path) {
|
||||
return remove(path) == 0;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDirEnsure(const char *dir) {
|
||||
struct stat st;
|
||||
|
||||
if (mkdir(dir, SAVE_DIR_MODE) == 0) {
|
||||
return true;
|
||||
}
|
||||
// mkdir failed -- success only if it failed because a directory is there.
|
||||
return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
|
||||
}
|
||||
27
src/generic/genericSave.c
Normal file
27
src/generic/genericSave.c
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Generic portable-C no-op defaults for the save-file + disk-space service.
|
||||
// A port with no writable filesystem (or the BLANK template) links these: the
|
||||
// SAVES/ directory cannot be made, nothing can be deleted, and free space is
|
||||
// unknown -- so jlSaveWrite/Open fail cleanly and jlDiskFree returns 0. Every
|
||||
// real port overrides via JL_HAS_SAVE_DIR_ENSURE / JL_HAS_SAVE_DELETE /
|
||||
// JL_HAS_DISK_FREE. Always compiled in via the src/generic/*.c wildcard;
|
||||
// dead-stripped where overridden.
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
uint32_t jlpGenericDiskFree(void) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
bool jlpGenericSaveDelete(const char *path) {
|
||||
(void)path;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool jlpGenericSaveDirEnsure(const char *dir) {
|
||||
(void)dir;
|
||||
return false;
|
||||
}
|
||||
177
src/iigs/save.c
Normal file
177
src/iigs/save.c
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Apple IIgs save-file HAL.
|
||||
//
|
||||
// GS/OS-backed stdio (fopen/fread/fwrite -- the runtime's libc GS/OS layer)
|
||||
// handles the blob + stream paths, so jlSaveOpen/Write/Read/Exists just work.
|
||||
// The three ops stdio cannot express are issued as GS/OS class-1 calls through
|
||||
// the runtime wrappers in <iigs/gsos.h>:
|
||||
//
|
||||
// - delete: remove() (maps to GS/OS Destroy).
|
||||
// - directory create: GS/OS Create ($2001) with storageType 13 (directory).
|
||||
// - disk free: the volume that holds the default prefix (prefix 0, which
|
||||
// unrooted paths like "SAVES/x" resolve against) does not have a name the
|
||||
// Volume call accepts directly -- Volume ($2008) wants a DEVICE name. So
|
||||
// the chain is GetPrefix(0) -> ":VOL:..." -> GetDevNumber(":VOL") -> devNum
|
||||
// -> DInfo(devNum) -> device name ".XXX" -> Volume(".XXX") -> free blocks.
|
||||
//
|
||||
// Every GS/OS call is guarded by __gsosAvailable(): when only the universal-
|
||||
// success stub is linked (sysless smoke harness) the wrappers are not real, so
|
||||
// dir-ensure falls back to trusting a prestaged SAVES/ and disk-free reports 0.
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <iigs/gsos.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
// GS/OS length-prefixed input string, sized for our short save/device paths.
|
||||
typedef struct {
|
||||
unsigned short length;
|
||||
char text[64];
|
||||
} GsInStr;
|
||||
|
||||
// GS/OS ResultBuf (maxLen + length + text), sized for a prefix / name string.
|
||||
typedef struct {
|
||||
unsigned short maxLen;
|
||||
unsigned short length;
|
||||
char text[80];
|
||||
} GsResult;
|
||||
|
||||
#define GSOS_ERR_DUP 0x47u // duplicate pathname (already exists)
|
||||
#define GSOS_DIR_TYPE 0x0Fu // directory file type
|
||||
#define GSOS_DIR_STORE 0x000Du // storageType 13 = directory
|
||||
#define GSOS_FULL_ACCESS 0xC3u // read/write/rename/destroy enabled
|
||||
#define IIGS_MAX_U32 0xFFFFFFFFu
|
||||
|
||||
|
||||
static void gsStrSet(GsInStr *dst, const char *src, unsigned short cap);
|
||||
|
||||
|
||||
// Copy a C string into a GS/OS length-prefixed string (truncating at cap).
|
||||
static void gsStrSet(GsInStr *dst, const char *src, unsigned short cap) {
|
||||
unsigned short i;
|
||||
|
||||
i = 0u;
|
||||
while (src[i] != '\0' && i < cap) {
|
||||
dst->text[i] = src[i];
|
||||
i++;
|
||||
}
|
||||
dst->length = i;
|
||||
}
|
||||
|
||||
|
||||
uint32_t jlpDiskFree(void) {
|
||||
// Static (not stack): keeps this leaf HAL off the shallow IIgs soft stack.
|
||||
static GsResult prefix;
|
||||
static GsResult devNameBuf;
|
||||
static GsResult volNameOut;
|
||||
static GsInStr volArg;
|
||||
static GsInStr devArg;
|
||||
PrefixRecGS pr;
|
||||
DevNumRecGS dn;
|
||||
DInfoRecGS di;
|
||||
VolumeRecGS vr;
|
||||
unsigned short i;
|
||||
unsigned short n;
|
||||
unsigned short cut;
|
||||
unsigned short bytesPerBlock;
|
||||
|
||||
if (!__gsosAvailable()) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
// Prefix 0 is what unrooted paths (like "SAVES/x") resolve against, so its
|
||||
// volume is the one SAVES/ lives on. It comes back in ":VOL:..." form.
|
||||
prefix.maxLen = (unsigned short)(sizeof(prefix) - 2u);
|
||||
pr.pCount = 2u;
|
||||
pr.prefixNum = 0u;
|
||||
pr.prefix = &prefix;
|
||||
if (gsosGetPrefix(&pr) != 0u || prefix.length == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
// Extract the leading volume component ":VOL" (up to the 2nd separator;
|
||||
// '/' on ProDOS, ':' in GS/OS native form).
|
||||
cut = prefix.length;
|
||||
for (i = 1u; i < prefix.length; i++) {
|
||||
if (prefix.text[i] == '/' || prefix.text[i] == ':') {
|
||||
cut = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
n = 0u;
|
||||
for (i = 0u; i < cut && n < (unsigned short)sizeof(volArg.text); i++) {
|
||||
volArg.text[n++] = prefix.text[i];
|
||||
}
|
||||
volArg.length = n;
|
||||
|
||||
// Volume name -> device number -> device name (what Volume needs).
|
||||
dn.pCount = 2u;
|
||||
dn.devName = &volArg;
|
||||
dn.devNum = 0u;
|
||||
if (gsosGetDevNumber(&dn) != 0u) {
|
||||
return 0u;
|
||||
}
|
||||
devNameBuf.maxLen = (unsigned short)(sizeof(devNameBuf) - 2u);
|
||||
di.pCount = 2u;
|
||||
di.devNum = dn.devNum;
|
||||
di.devName = &devNameBuf;
|
||||
if (gsosDInfo(&di) != 0u || devNameBuf.length == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
n = 0u;
|
||||
for (i = 0u; i < devNameBuf.length && n < (unsigned short)sizeof(devArg.text); i++) {
|
||||
devArg.text[n++] = devNameBuf.text[i];
|
||||
}
|
||||
devArg.length = n;
|
||||
|
||||
// Device name -> free blocks + block size.
|
||||
volNameOut.maxLen = (unsigned short)(sizeof(volNameOut) - 2u);
|
||||
vr.pCount = 6u;
|
||||
vr.devName = &devArg;
|
||||
vr.volName = &volNameOut;
|
||||
vr.totalBlocks = 0uL;
|
||||
vr.freeBlocks = 0uL;
|
||||
vr.fileSysID = 0u;
|
||||
vr.blockSize = 0u;
|
||||
if (gsosVolume(&vr) != 0u) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
// free bytes = freeBlocks * blockSize, saturating in 32-bit.
|
||||
bytesPerBlock = vr.blockSize;
|
||||
if (bytesPerBlock == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
if (vr.freeBlocks > IIGS_MAX_U32 / bytesPerBlock) {
|
||||
return IIGS_MAX_U32;
|
||||
}
|
||||
return (uint32_t)(vr.freeBlocks * bytesPerBlock);
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDelete(const char *path) {
|
||||
return remove(path) == 0;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDirEnsure(const char *dir) {
|
||||
GsInStr name;
|
||||
CreateRecGS cr;
|
||||
unsigned short err;
|
||||
|
||||
if (!__gsosAvailable()) {
|
||||
return true; // no real GS/OS surface -- trust a prestaged SAVES/
|
||||
}
|
||||
gsStrSet(&name, dir, (unsigned short)sizeof(name.text));
|
||||
|
||||
cr.pCount = 5u;
|
||||
cr.pathname = &name;
|
||||
cr.access = GSOS_FULL_ACCESS;
|
||||
cr.fileType = GSOS_DIR_TYPE;
|
||||
cr.auxType = 0uL;
|
||||
cr.storageType = GSOS_DIR_STORE;
|
||||
err = gsosCreate(&cr);
|
||||
return err == 0u || err == GSOS_ERR_DUP; // created, or already present
|
||||
}
|
||||
137
tests/host/saveHost.c
Normal file
137
tests/host/saveHost.c
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// saveHost.c - Host-cc harness for the save-file core (src/core/save.c).
|
||||
//
|
||||
// Built by scripts/check-save.sh on the serialHost.c pattern: compiled with
|
||||
// -DJOEYLIB_PLATFORM_BLANK plus the JL_HAS_SAVE_* flags so port.h routes
|
||||
// jlpSave*/jlpDiskFree to the POSIX HAL below. Unlike the serial mock, this
|
||||
// exercises the REAL core end to end -- jlSaveWrite/Read/Open/Exists/Delete do
|
||||
// actual stdio into a SAVES/ directory in the current working directory, which
|
||||
// check-save.sh points at a throwaway temp dir. Prints one line per check and
|
||||
// a final tally; exits nonzero on any failure, so it doubles as a CI gate.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
// ----- POSIX save HAL (satisfies the jlpSave* dispatch under -DJL_HAS_SAVE_*) -----
|
||||
|
||||
uint32_t jlpDiskFree(void) {
|
||||
struct statvfs vfs;
|
||||
unsigned long long bytes;
|
||||
|
||||
if (statvfs(".", &vfs) != 0) {
|
||||
return 0u;
|
||||
}
|
||||
bytes = (unsigned long long)vfs.f_bavail * (unsigned long long)vfs.f_frsize;
|
||||
if (bytes > 0xFFFFFFFFull) {
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
return (uint32_t)bytes;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDelete(const char *path) {
|
||||
return remove(path) == 0;
|
||||
}
|
||||
|
||||
|
||||
bool jlpSaveDirEnsure(const char *dir) {
|
||||
struct stat st;
|
||||
|
||||
if (mkdir(dir, 0777) == 0) {
|
||||
return true;
|
||||
}
|
||||
return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
|
||||
// ----- Test harness -----
|
||||
|
||||
static int gPass = 0;
|
||||
static int gFail = 0;
|
||||
|
||||
|
||||
static void check(const char *name, bool ok) {
|
||||
if (ok) {
|
||||
gPass++;
|
||||
printf(" ok %s\n", name);
|
||||
} else {
|
||||
gFail++;
|
||||
printf(" FAIL %s\n", name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
uint8_t out[64];
|
||||
uint8_t in[64];
|
||||
uint32_t n;
|
||||
FILE *fp;
|
||||
int i;
|
||||
|
||||
printf("save-host: core save/read/exists/delete/diskfree (real files)\n");
|
||||
|
||||
for (i = 0; i < 64; i++) {
|
||||
out[i] = (uint8_t)(i * 7 + 1);
|
||||
}
|
||||
|
||||
// 1. Clean slate + a plausible free-space number.
|
||||
check("fresh: hero.sav absent", jlSaveExists("hero.sav") == false);
|
||||
check("diskFree > 0", jlDiskFree() > 0u);
|
||||
|
||||
// 2. Blob write creates SAVES/ on demand and round-trips.
|
||||
check("write blob (64)", jlSaveWrite("hero.sav", out, 64u) == true);
|
||||
check("exists after write", jlSaveExists("hero.sav") == true);
|
||||
memset(in, 0, sizeof(in));
|
||||
n = jlSaveRead("hero.sav", in, sizeof(in));
|
||||
check("read full length", n == 64u);
|
||||
check("payload round-trips", memcmp(in, out, 64) == 0);
|
||||
|
||||
// 3. Read honors the caller's max.
|
||||
memset(in, 0, sizeof(in));
|
||||
n = jlSaveRead("hero.sav", in, 16u);
|
||||
check("read capped at max", n == 16u);
|
||||
check("capped payload matches", memcmp(in, out, 16) == 0);
|
||||
|
||||
// 4. Streaming open for incremental writes.
|
||||
fp = jlSaveOpen("stream.sav", "wb");
|
||||
check("stream open (wb)", fp != NULL);
|
||||
if (fp != NULL) {
|
||||
fputc('J', fp);
|
||||
fputc('L', fp);
|
||||
fclose(fp);
|
||||
}
|
||||
memset(in, 0, sizeof(in));
|
||||
n = jlSaveRead("stream.sav", in, sizeof(in));
|
||||
check("stream read length 2", n == 2u);
|
||||
check("stream payload 'JL'", in[0] == (uint8_t)'J' && in[1] == (uint8_t)'L');
|
||||
|
||||
// 5. Zero-length write makes an empty file.
|
||||
check("write empty", jlSaveWrite("empty.sav", NULL, 0u) == true);
|
||||
check("empty exists", jlSaveExists("empty.sav") == true);
|
||||
check("empty reads 0", jlSaveRead("empty.sav", in, sizeof(in)) == 0u);
|
||||
|
||||
// 6. Delete removes it; deleting a missing file reports false.
|
||||
check("delete hero.sav", jlSaveDelete("hero.sav") == true);
|
||||
check("hero.sav gone", jlSaveExists("hero.sav") == false);
|
||||
check("delete missing -> false", jlSaveDelete("nope.sav") == false);
|
||||
|
||||
// 7. NULL / bad-argument guards.
|
||||
check("write NULL name", jlSaveWrite(NULL, out, 4u) == false);
|
||||
check("read NULL name", jlSaveRead(NULL, in, 4u) == 0u);
|
||||
check("read NULL buf", jlSaveRead("empty.sav", NULL, 4u) == 0u);
|
||||
check("open NULL name", jlSaveOpen(NULL, "rb") == NULL);
|
||||
check("exists NULL name", jlSaveExists(NULL) == false);
|
||||
check("delete NULL name", jlSaveDelete(NULL) == false);
|
||||
|
||||
// Best-effort cleanup of the files this run created.
|
||||
jlSaveDelete("stream.sav");
|
||||
jlSaveDelete("empty.sav");
|
||||
|
||||
printf("save-host: %d passed, %d failed\n", gPass, gFail);
|
||||
return (gFail == 0) ? 0 : 1;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue