pxl8/src/asset/pxl8_save.c

221 lines
5.7 KiB
C
Raw Normal View History

#include "pxl8_save.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "pxl8_io.h"
2025-12-02 11:02:23 -06:00
#include "pxl8_log.h"
#include "pxl8_macros.h"
2026-01-21 23:19:50 -06:00
#include "pxl8_mem.h"
#define PATH_SEP '/'
typedef struct {
u32 magic;
u32 version;
u32 size;
u32 checksum;
} pxl8_save_header;
struct pxl8_save {
char directory[PXL8_SAVE_MAX_PATH];
u32 magic;
u32 version;
};
static u32 pxl8_save_checksum(const u8* data, u32 size) {
u32 hash = 2166136261u;
for (u32 i = 0; i < size; i++) {
hash ^= data[i];
hash *= 16777619u;
}
return hash;
}
2026-01-21 23:19:50 -06:00
static void pxl8_save_get_slot_path(pxl8_save* save, u8 slot, char* path, usize path_size) {
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
snprintf(path, path_size, "%s%chotreload.sav", save->directory, PATH_SEP);
} else {
snprintf(path, path_size, "%s%csave%d.sav", save->directory, PATH_SEP, slot);
}
}
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version) {
if (!game_name) return NULL;
2026-01-21 23:19:50 -06:00
pxl8_save* save = (pxl8_save*)pxl8_calloc(1, sizeof(pxl8_save));
if (!save) return NULL;
save->magic = magic;
save->version = version;
char* pref_path = pxl8_io_get_pref_path("pxl8", game_name);
if (!pref_path) {
2026-01-21 23:19:50 -06:00
pxl8_free(save);
return NULL;
}
pxl8_strncpy(save->directory, pref_path, sizeof(save->directory));
pxl8_free(pref_path);
usize dir_len = strlen(save->directory);
if (dir_len > 0 && save->directory[dir_len - 1] == '/') {
save->directory[dir_len - 1] = '\0';
}
pxl8_info("Save system initialized: %s", save->directory);
return save;
}
void pxl8_save_destroy(pxl8_save* save) {
pxl8_free(save);
}
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (!data || size == 0) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
FILE* file = fopen(path, "wb");
if (!file) {
pxl8_error("Failed to open save file for writing: %s", path);
return PXL8_ERROR_SYSTEM_FAILURE;
}
pxl8_save_header header = {
.magic = save->magic,
.version = save->version,
.size = size,
.checksum = pxl8_save_checksum(data, size)
};
bool success = true;
if (fwrite(&header, sizeof(header), 1, file) != 1) success = false;
if (success && fwrite(data, 1, size, file) != size) success = false;
fclose(file);
if (!success) {
pxl8_error("Failed to write save data");
return PXL8_ERROR_SYSTEM_FAILURE;
}
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
pxl8_debug("Hot reload state saved (%u bytes)", size);
} else {
pxl8_info("Game saved to slot %d (%u bytes)", slot, size);
}
return PXL8_OK;
}
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (!data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
*data_out = NULL;
*size_out = 0;
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
FILE* file = fopen(path, "rb");
if (!file) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
pxl8_save_header header;
if (fread(&header, sizeof(header), 1, file) != 1) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
if (header.magic != save->magic) {
fclose(file);
pxl8_error("Invalid save file magic");
return PXL8_ERROR_INVALID_FORMAT;
}
if (header.version > save->version) {
fclose(file);
pxl8_error("Save file version too new: %u", header.version);
return PXL8_ERROR_INVALID_FORMAT;
}
2026-01-21 23:19:50 -06:00
u8* data = (u8*)pxl8_malloc(header.size);
if (!data) {
fclose(file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (fread(data, 1, header.size, file) != header.size) {
2026-01-21 23:19:50 -06:00
pxl8_free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
u32 checksum = pxl8_save_checksum(data, header.size);
if (checksum != header.checksum) {
2026-01-21 23:19:50 -06:00
pxl8_free(data);
pxl8_error("Save file checksum mismatch");
return PXL8_ERROR_INVALID_FORMAT;
}
*data_out = data;
*size_out = header.size;
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
pxl8_debug("Hot reload state loaded (%u bytes)", header.size);
} else {
pxl8_info("Game loaded from slot %d (%u bytes)", slot, header.size);
}
return PXL8_OK;
}
void pxl8_save_free(u8* data) {
pxl8_free(data);
}
bool pxl8_save_exists(pxl8_save* save, u8 slot) {
if (!save) return false;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return false;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
return pxl8_io_file_exists(path);
}
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
if (remove(path) != 0 && errno != ENOENT) {
return PXL8_ERROR_SYSTEM_FAILURE;
}
return PXL8_OK;
}
const char* pxl8_save_get_directory(pxl8_save* save) {
return save ? save->directory : NULL;
}