Dawn/src/dawn/save/SaveManager.cpp

217 lines
6.1 KiB
C++

// 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<int16_t> SaveManager::getUsedSlots() {
std::vector<int16_t> slots;
return slots;
}