273 lines
6.9 KiB
C
273 lines
6.9 KiB
C
#include "pxl8_save.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#include <shlobj.h>
|
|
#define PATH_SEP '\\'
|
|
#else
|
|
#include <unistd.h>
|
|
#include <pwd.h>
|
|
#define PATH_SEP '/'
|
|
#endif
|
|
|
|
#include "pxl8_log.h"
|
|
#include "pxl8_mem.h"
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
static pxl8_result pxl8_save_ensure_directory(const char* path) {
|
|
struct stat st;
|
|
if (stat(path, &st) == 0) {
|
|
return S_ISDIR(st.st_mode) ? PXL8_OK : PXL8_ERROR_SYSTEM_FAILURE;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
if (_mkdir(path) != 0 && errno != EEXIST) {
|
|
#else
|
|
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
|
|
#endif
|
|
return PXL8_ERROR_SYSTEM_FAILURE;
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version) {
|
|
if (!game_name) return NULL;
|
|
|
|
pxl8_save* save = (pxl8_save*)pxl8_calloc(1, sizeof(pxl8_save));
|
|
if (!save) return NULL;
|
|
|
|
save->magic = magic;
|
|
save->version = version;
|
|
|
|
char base_dir[PXL8_SAVE_MAX_PATH];
|
|
|
|
#ifdef _WIN32
|
|
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, base_dir))) {
|
|
snprintf(save->directory, sizeof(save->directory),
|
|
"%s%cpxl8%c%s", base_dir, PATH_SEP, PATH_SEP, game_name);
|
|
} else {
|
|
pxl8_free(save);
|
|
return NULL;
|
|
}
|
|
#else
|
|
const char* home = getenv("HOME");
|
|
if (!home) {
|
|
struct passwd* pw = getpwuid(getuid());
|
|
if (pw) home = pw->pw_dir;
|
|
}
|
|
if (!home) {
|
|
pxl8_free(save);
|
|
return NULL;
|
|
}
|
|
|
|
snprintf(base_dir, sizeof(base_dir), "%s/.local/share", home);
|
|
pxl8_save_ensure_directory(base_dir);
|
|
|
|
snprintf(base_dir, sizeof(base_dir), "%s/.local/share/pxl8", home);
|
|
pxl8_save_ensure_directory(base_dir);
|
|
|
|
snprintf(save->directory, sizeof(save->directory),
|
|
"%s/.local/share/pxl8/%s", home, game_name);
|
|
#endif
|
|
|
|
if (pxl8_save_ensure_directory(save->directory) != PXL8_OK) {
|
|
pxl8_free(save);
|
|
return NULL;
|
|
}
|
|
|
|
pxl8_info("Save system initialized: %s", save->directory);
|
|
return save;
|
|
}
|
|
|
|
void pxl8_save_destroy(pxl8_save* save) {
|
|
if (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;
|
|
}
|
|
|
|
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) {
|
|
pxl8_free(data);
|
|
fclose(file);
|
|
return PXL8_ERROR_SYSTEM_FAILURE;
|
|
}
|
|
|
|
fclose(file);
|
|
|
|
u32 checksum = pxl8_save_checksum(data, header.size);
|
|
if (checksum != header.checksum) {
|
|
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) {
|
|
if (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));
|
|
|
|
struct stat st;
|
|
return stat(path, &st) == 0;
|
|
}
|
|
|
|
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;
|
|
}
|