Save Manager
This commit is contained in:
11
src/dawn/save/CMakeLists.txt
Normal file
11
src/dawn/save/CMakeLists.txt
Normal file
@ -0,0 +1,11 @@
|
||||
# Copyright (c) 2022 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DAWN_TARGET_NAME}
|
||||
PRIVATE
|
||||
SaveFile.cpp
|
||||
SaveManager.cpp
|
||||
)
|
32
src/dawn/save/SaveFile.cpp
Normal file
32
src/dawn/save/SaveFile.cpp
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2022 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
#include "SaveFile.hpp"
|
||||
|
||||
using namespace Dawn;
|
||||
|
||||
bool_t SaveFile::has(std::string key) {
|
||||
auto exist = this->values.find(key);
|
||||
return exist != this->values.end();
|
||||
}
|
||||
|
||||
savedata_t SaveFile::get(std::string key) {
|
||||
auto exist = this->values.find(key);
|
||||
assertTrue(exist != this->values.end());
|
||||
return exist->second;
|
||||
}
|
||||
|
||||
void SaveFile::set(std::string key, savedata_t value) {
|
||||
this->hasChanges = true;
|
||||
this->values[key] = value;
|
||||
}
|
||||
|
||||
void SaveFile::copy(struct SaveFile raw, std::string key) {
|
||||
this->set(key, raw.get(key));
|
||||
}
|
||||
|
||||
void SaveFile::reset() {
|
||||
this->values.clear();
|
||||
}
|
63
src/dawn/save/SaveFile.hpp
Normal file
63
src/dawn/save/SaveFile.hpp
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2022 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
#pragma once
|
||||
#include "dawnlibs.hpp"
|
||||
#include "assert/assert.hpp"
|
||||
#include "util/array.hpp"
|
||||
|
||||
namespace Dawn {
|
||||
typedef union {
|
||||
float_t f;
|
||||
int64_t i64;
|
||||
int32_t i32;
|
||||
uint32_t u32;
|
||||
int16_t i16;
|
||||
uint16_t u16;
|
||||
uint8_t u8;
|
||||
int8_t i8;
|
||||
} savedata_t;
|
||||
|
||||
struct SaveFile {
|
||||
bool_t hasChanges;
|
||||
std::map<std::string, savedata_t> values;
|
||||
// std::map<std::string, std::string> stringValues;
|
||||
// std::map<std::string, std::vector<savedata_t>> arrayValues;
|
||||
|
||||
/**
|
||||
* Returns true if the given key exists in the save file.
|
||||
*
|
||||
* @param key Key to check if exists.
|
||||
* @return True if exists.
|
||||
*/
|
||||
bool_t has(std::string key);
|
||||
|
||||
/**
|
||||
* Returns the value of a given key.
|
||||
*
|
||||
* @param key Key to get the value of.
|
||||
* @return The value.
|
||||
*/
|
||||
savedata_t get(std::string key);
|
||||
|
||||
// std::string getString(std::string key);
|
||||
// std::vector<savedata_t> getArray(std::string key);
|
||||
|
||||
/**
|
||||
* Sets the given value to the save file.
|
||||
*
|
||||
* @param key Key to set.
|
||||
* @param data Data to set.
|
||||
*/
|
||||
void set(std::string key, savedata_t data);
|
||||
|
||||
void copy(struct SaveFile raw, std::string key);
|
||||
|
||||
/**
|
||||
* Completely resets the save file (empties all the values).
|
||||
*/
|
||||
void reset();
|
||||
};
|
||||
}
|
217
src/dawn/save/SaveManager.cpp
Normal file
217
src/dawn/save/SaveManager.cpp
Normal file
@ -0,0 +1,217 @@
|
||||
// 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(this->validateSave(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;
|
||||
}
|
91
src/dawn/save/SaveManager.hpp
Normal file
91
src/dawn/save/SaveManager.hpp
Normal file
@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2022 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
#pragma once
|
||||
#include "SaveFile.hpp"
|
||||
#include "util/string.hpp"
|
||||
|
||||
#define SAVE_KEY_TIMESTAMP "meta.timestamp"
|
||||
#define SAVE_KEY_SLOT "meta.slot"
|
||||
#define SAVE_MANAGER_BUFFER_LENGTH 128
|
||||
#define SAVE_MANAGER_FILENAME_LENGTH 512
|
||||
|
||||
namespace Dawn {
|
||||
class DawnGame;
|
||||
|
||||
enum SaveLoadResult {
|
||||
SAVE_LOAD_SUCCESS,
|
||||
SAVE_LOAD_RESULT_FILE_NOT_PRESENT,
|
||||
SAVE_LOAD_RESULT_TOO_SMALL,
|
||||
SAVE_LOAD_RESULT_CORRUPTED_DE_SAVE,
|
||||
SAVE_LOAD_RESULT_CORRUPTED_VERSION,
|
||||
SAVE_LOAD_RESULT_CORRUPTED_TIMESTAMP,
|
||||
SAVE_LOAD_RESULT_CORRUPTED_DATA_KEY,
|
||||
SAVE_LOAD_RESULT_CORRUPTED_DATA_VALUE,
|
||||
SAVE_LOAD_RESULT_MISMATCH_TIMESTAMP,
|
||||
SAVE_LOAD_RESULT_ERROR
|
||||
};
|
||||
|
||||
class SaveManager {
|
||||
protected:
|
||||
int16_t currentSaveSlot = -1;
|
||||
struct SaveFile currentSave;
|
||||
|
||||
/**
|
||||
* Reads a save file and then validates all of the values. If a value is
|
||||
* not validated then it will NOT be retained.
|
||||
*
|
||||
* @param file Save file to read from.
|
||||
* @return Whether or not the save file is corrupt, false for not corrupt.
|
||||
*/
|
||||
virtual bool_t validateSave(struct SaveFile raw) = 0;
|
||||
|
||||
public:
|
||||
DawnGame *game;
|
||||
|
||||
/**
|
||||
* Create a new Save Manager. Used by the game instance to decide when/how
|
||||
* to perform saving.
|
||||
*
|
||||
* @param game Game that this save manager belongs to.
|
||||
*/
|
||||
SaveManager(DawnGame *game);
|
||||
|
||||
/**
|
||||
* Immediately save the game to the given file. This will invoke the
|
||||
* game's custom writer to write the output data to the save file.
|
||||
*/
|
||||
void saveFile();
|
||||
|
||||
/**
|
||||
* Loads the current slotted save file. Invokes the internal managers read
|
||||
* function.
|
||||
*/
|
||||
enum SaveLoadResult loadFile();
|
||||
|
||||
/**
|
||||
* Deletes the given save file slot. We use slots here because we need to
|
||||
* be able to delete a file without reading it (in case of corruption).
|
||||
*
|
||||
* @param slot Slot to delete.
|
||||
*/
|
||||
void deleteFile(int16_t slot);
|
||||
|
||||
/**
|
||||
* Determines which save slot to use.
|
||||
*
|
||||
* @param slot Save slot to use.
|
||||
*/
|
||||
void useSlot(int16_t slot);
|
||||
|
||||
/**
|
||||
* Returns a list of used save slots, does not confirm if they are corrupt
|
||||
* or not, just whether they are in existance or not.
|
||||
*
|
||||
* @return List of used save slots.
|
||||
*/
|
||||
std::vector<int16_t> getUsedSlots();
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user