#include "pxl8_save.h" #include #include #include #include "pxl8_io.h" #include "pxl8_log.h" #include "pxl8_macros.h" #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; } 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; 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) { 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; } 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) { 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; }