#include "pxl8_save.h" #include #include #include #include #include #ifdef _WIN32 #include #include #define PATH_SEP '\\' #else #include #include #define PATH_SEP '/' #endif #include "pxl8_macros.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, size_t 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*)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 { 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) { 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) { free(save); return NULL; } pxl8_info("Save system initialized: %s", save->directory); return save; } void pxl8_save_destroy(pxl8_save* save) { if (save) { 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*)malloc(header.size); if (!data) { fclose(file); return PXL8_ERROR_OUT_OF_MEMORY; } if (fread(data, 1, header.size, file) != header.size) { free(data); fclose(file); return PXL8_ERROR_SYSTEM_FAILURE; } fclose(file); u32 checksum = pxl8_save_checksum(data, header.size); if (checksum != header.checksum) { 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) { 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; }