// Copyright (c) 2022 Dominic Masters // // This software is released under the MIT License. // https://opensource.org/licenses/MIT #include "SaveManager.hpp" #include "game/DawnGame.hpp" using namespace Dawn; SaveManager::SaveManager(DawnGame *game) { this->game = game; } void SaveManager::saveFile() { savedata_t value; assertTrue(this->currentSaveSlot >= 0); // Update metadata auto timestamp = this->game->timeManager.getTimestamp(); value.i64 = timestamp; this->currentSave.set(SAVE_KEY_TIMESTAMP, value); value.i16 = this->currentSaveSlot; this->currentSave.set(SAVE_KEY_SLOT, value); // Now open output buffer. char filename[SAVE_MANAGER_FILENAME_LENGTH]; sprintf(filename, "savefile_%u.save", this->currentSaveSlot); FILE *fptr = fopen(filename, "wb"); if(fptr == NULL) { printf("Error opening %s\n", filename); assertUnreachable(); return; } // Now begin buffering data. First we start with the common phrase, the engine // version, the timestamp. In future we will store the MD5 here too. char buffer[SAVE_MANAGER_BUFFER_LENGTH]; auto len = sprintf(buffer, "DE_SAVE|%s|%lld|", "1.00", timestamp); fwrite(buffer, sizeof(char), len, fptr); // Now buffer out the data, this is almost certain to change over time. auto it = this->currentSave.values.begin(); while(it != this->currentSave.values.end()) { uint8_t *buffer = (uint8_t *)&it->second; char delim = '|'; fwrite(it->first.c_str(), sizeof(char), strlen(it->first.c_str()), fptr); fwrite(&delim, sizeof(char), 1, fptr); fwrite(buffer, sizeof(uint8_t), sizeof(savedata_t) / sizeof(uint8_t), fptr); fwrite(&delim, sizeof(char), 1, fptr); ++it; } // Buffering done, close the file buffer. fclose(fptr); this->currentSave.hasChanges = false; } enum SaveLoadResult SaveManager::loadFile() { this->currentSave.reset(); assertTrue(this->currentSaveSlot >= 0); // Load file struct SaveFile temporaryFile; char filename[SAVE_MANAGER_FILENAME_LENGTH]; sprintf(filename, "savefile_%u.save", this->currentSaveSlot); FILE *fptr = fopen(filename, "rb"); if(fptr == NULL) { return SAVE_LOAD_RESULT_FILE_NOT_PRESENT; } // Let's read how long the file is first. fseek(fptr, 0, SEEK_END); auto len = ftell(fptr); // Now let's just ensure the file isn't too small, this starts by us making // sure we have at least "DE_SAVE|%s|%lld|", the string is unknown length, // but we can be 100% sure of the rest of the file size. So we take // 7+1+1+1+19+1=30 if(len < 30) { fclose(fptr); return SAVE_LOAD_RESULT_TOO_SMALL; } // Rewind. fseek(fptr, 0, SEEK_SET); // Ok let's buffer the first 8 characters to validate "DE_SAVE|" char buffer[SAVE_MANAGER_BUFFER_LENGTH]; auto read = fread(buffer, sizeof(char), SAVE_MANAGER_BUFFER_LENGTH, fptr); if( read < 8 || buffer[0] != 'D' || buffer[1] != 'E' || buffer[2] != '_' || buffer[3] != 'S' || buffer[4] != 'A' || buffer[5] != 'V' || buffer[6] != 'E' || buffer[7] != '|' ) { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_DE_SAVE; } // Now read ahead to the next vertical bar, that will give us the engine // version char *bufferCurrent = buffer + 8; char *p = stringFindNext(bufferCurrent, '|', 10); if(p == NULL) { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_VERSION; } *p = '\0'; char *version = bufferCurrent; bufferCurrent = p + 1; // Now read the timestamp string. p = stringFindNext(bufferCurrent, '|', 64); if(p == NULL) { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_TIMESTAMP; } *p = '\0'; char *strTimestamp = bufferCurrent; // Parse timestamp int64_t timestamp = strtoll(strTimestamp, NULL, 10); if(timestamp == 0) { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_TIMESTAMP; } // Now begin parsing the data in the save file. We start here, at the end of // the previous string, so it will INCLUDE "|" at the first char. read = p - buffer; while(read < (len-1)) { // Rewind and then buffer the next set of bytes fseek(fptr, (long)read, SEEK_SET); if(fread(buffer, sizeof(char), SAVE_MANAGER_BUFFER_LENGTH, fptr) <= 1) { break; } // Because we finish the last string INCLUDING "|", then we skip it here. bufferCurrent = buffer + 1; // Now, read the key p = stringFindNext(bufferCurrent, '|', SAVE_MANAGER_BUFFER_LENGTH); if(p == NULL) { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_DATA_KEY; } *p = '\0'; char *key = bufferCurrent; // Validate the string lengths if(strlen(key) <= 0) { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_DATA_KEY; } // Now read the value bufferCurrent = p + 1; savedata_t destination; uint8_t *ptrValue = (uint8_t*)bufferCurrent; memoryCopy(ptrValue, &destination, sizeof(savedata_t)); // Now advance the pointer... p = bufferCurrent + sizeof(savedata_t); if(*p != '|') { fclose(fptr); return SAVE_LOAD_RESULT_CORRUPTED_DATA_VALUE; } // Set the value. temporaryFile.set(std::string(key), destination); read += (p - buffer); } // Close file fclose(fptr); // OK Let's validate that everything was read OK if(temporaryFile.get(SAVE_KEY_TIMESTAMP).i64 != timestamp) { return SAVE_LOAD_RESULT_MISMATCH_TIMESTAMP; } // Now pass off to the loader if(saveValidateFile(temporaryFile) != false) { return SAVE_LOAD_RESULT_ERROR; } // Unmark changes. In future I may need to do more complex testing, e.g. if // the game has a newer version that updates save files some how. this->currentSave.hasChanges = false; return SAVE_LOAD_SUCCESS; } void SaveManager::deleteFile(int16_t slot) { assertTrue(slot >= 0); } void SaveManager::useSlot(int16_t slot) { assertTrue(slot >= 0); this->currentSaveSlot = slot; this->currentSave.reset(); } std::vector SaveManager::getUsedSlots() { std::vector slots; return slots; }