Saved State Format

Share saved states and game save files for the Retron 5 and Retro Freak.
Forum rules
Retron 5 and it's predecessors are trademarked Hyperkin, Inc. Retron name is copyright Hyperkin, Inc. Retro Freak is trademarked CYBER Gadget Corporation. Retro Freak name is copyright CYBER Gadget Corporation. Content found in these forums may be copyright the respective developers and studios which said content is associated with. Retro5.net makes no claim legal or otherwise to this content and user submitted content is not screened. That being said, please refrain from posting or linking to full rom files so that everyone leaves us alone. Sometimes dogs are round.
User avatar
Retron Tèch
Hyperkin
Posts: 10
Joined: September 18th, 2014, 6:09 am
Location: Inside your RetroN 5

Re: Saved State Format

Post by Retron Tèch » January 19th, 2015, 12:08 am

Following are the source files used by RetroN 5 for manipulating the save data. Placed into the public domain - use as you desire.

dataFile.h

Code: Select all

#ifndef _DATAFILE_H
#define _DATAFILE_H

#define RETRON_DATA_MAGIC			(0x354E5452)	// "RTN5"
#define RETRON_DATA_FORMAT_VER		(1)
#define RETRON_DATA_TMP_POSTFIX		".TMP"

#define RETRON_DATA_FLG_ZLIB_PACKED	(0x01)

#define RETRON_DATA_DEFCOMPRESS		(true)
#define RETRON_DATA_RAMFILE			"/mnt/ram/tempdata.bin"

enum
{
	RETRON_DATA_SUCCESS = 0,
	RETRON_DATA_ERR_CORRUPT = -1,
	RETRON_DATA_ERR_IO = -2,
	RETRON_DATA_ERR_MEM = -3,
	RETRON_DATA_ERR_VERSION = -4,
	RETRON_DATA_ERR_MISC = -5,
};

typedef struct
{
	uint32_t magic;
	uint16_t fmtVer;
	uint16_t flags;
	uint32_t origSize;
	uint32_t packedSize;
	uint32_t dataOffset;
	uint32_t crc32;
	uint8_t data[0];
} t_retronDataHdr;

class cRetronData
{
public:

	cRetronData(const char *dataFilename);
	~cRetronData();
	int write(void *srcBuf, int srcBufSize, bool compress); // write from buffer into datafile
	int write(const char *srcFilename, bool compress); // write from normal file into datafile
	int read(void **destBuf, int *destBufSize); // read from datafile into buffer
	int read(const char *destFilename); // read from datafile into normal file

private:

	bool readFile(const char *filename, void **buffer, int *size);
	bool writeFile(const char *filename, void *buffer, int size);
	bool flushFs(const char *filename);
	int read(FILE *dataFd, void **destBuf, int *destBufSize);

	const char *mDataFilename, *mDataTmpFilename;
};

#endif
dataFile.cpp

Code: Select all

#include "retronEngine.h"

/*
 * Retron data file safety wrapper - protects data files (such as snapshots, sram states) with CRC32 and ensures FS integrity
 * by first writing to a temp file then switching via rename at the end, and flushing FS after use. Data files are optionally compressed
 */

#define FREAD_SAFE(ptr, bytes, fd)	{ int _n = fread((ptr), 1, (bytes), (fd)); if(_n != (bytes)) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define FWRITE_SAFE(ptr, bytes, fd)	{ int _n = fwrite((ptr), 1, (bytes), (fd)); if(_n != (bytes)) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define FSEEK_SAFE(fd, off, whence)	{ int _o = fseek((fd), (off), (whence)); if(_o < 0) { rv = RETRON_DATA_ERR_IO; goto end; } \
										if(((whence) == SEEK_SET) && (ftell((fd)) != (off))) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define BAIL(code)					{ rv = code; goto end; }

cRetronData::cRetronData(const char *dataFilename)
{
	mDataFilename = strdup(dataFilename);
	assert(dataFilename != NULL);
	int tmpLen = strlen(mDataFilename) + strlen(RETRON_DATA_TMP_POSTFIX) + 1;
	char *tmpName = (char *)malloc(tmpLen);
	assert(tmpName != NULL);
	strcpy(tmpName, (char *)mDataFilename);
	strcat(tmpName, RETRON_DATA_TMP_POSTFIX);
	mDataTmpFilename = tmpName;
}

cRetronData::~cRetronData()
{
	if(mDataFilename)
		free((void *)mDataFilename);
	if(mDataTmpFilename)
		free((void *)mDataTmpFilename);
}

int cRetronData::write(void *srcBuf, int srcBufSize, bool compress)
{
	t_retronDataHdr hdr;
	FILE *fd = NULL;
	int rv = RETRON_DATA_ERR_IO;
	Bytef *destBuf = NULL;

	memset(&hdr, 0, sizeof(hdr));
	hdr.magic = RETRON_DATA_MAGIC;
	hdr.fmtVer = RETRON_DATA_FORMAT_VER;
	hdr.dataOffset = sizeof(hdr);
	hdr.crc32 = crc32(0, (const Bytef *)srcBuf, srcBufSize);
	hdr.origSize = srcBufSize;

	fd = fopen(mDataTmpFilename, "wb+");
	if(!fd)
		BAIL(RETRON_DATA_ERR_IO);

	if(compress)
	{
		hdr.flags |= RETRON_DATA_FORMAT_VER;

		uLongf destLen = compressBound(srcBufSize);
		destBuf = (Bytef *)malloc(destLen);
		if(!destBuf)
			BAIL(RETRON_DATA_ERR_MEM);
		rv = compress2(destBuf, &destLen, (const Bytef *)srcBuf, srcBufSize, Z_DEFAULT_COMPRESSION);
		if(rv != Z_OK)
			BAIL(RETRON_DATA_ERR_MISC);

		hdr.packedSize = destLen;
		FWRITE_SAFE(&hdr, sizeof(hdr), fd);
		FWRITE_SAFE(destBuf, destLen, fd);
	}
	else
	{
		FWRITE_SAFE(&hdr, sizeof(hdr), fd);
		FWRITE_SAFE(srcBuf, srcBufSize, fd);
	}

	fclose(fd);
	fd = NULL;
	if(flushFs(mDataTmpFilename) != true)
		BAIL(RETRON_DATA_ERR_IO);
	// at this point the filesystem should have a clean copy of the datafile at the temp location

	// only try to remove it file already exists
	struct stat st;
	if(lstat(mDataFilename, &st) >= 0)
	{
		if(remove(mDataFilename) < 0)
		{
			LOGI("bail 1 %d\n", rv);
			BAIL(RETRON_DATA_ERR_IO);
		}
	}
	if(rename(mDataTmpFilename, mDataFilename) < 0)
	{
		LOGI("bail 2\n");
		BAIL(RETRON_DATA_ERR_IO);
	}
	if(flushFs(mDataTmpFilename) != true)
		BAIL(RETRON_DATA_ERR_IO);

	// at this point, the old file has been removed and the temp file renamed, and everything flushed - we're done
	rv = RETRON_DATA_SUCCESS;

end:
	if(fd)
		fclose(fd);
	if(destBuf)
		free(destBuf);
	LOGI("datafile write rv: %d\n", rv);
	return rv;
}

// TODO: implement proper streaming code, so we don't need to read the entire file in first
int cRetronData::write(const char *srcFilename, bool compress)
{
	void *buffer = NULL;
	int fileSize = 0;
	if(readFile(srcFilename, &buffer, &fileSize) != true)
	{
		LOGE("retron dat failed to read file\n");
		return RETRON_DATA_ERR_IO;
	}
	int rv = write(buffer, fileSize, compress);
	free(buffer);
	return rv;
}

int cRetronData::read(FILE *dataFd, void **destBuf, int *destBufSize)
{
	t_retronDataHdr hdr;
	int rv = RETRON_DATA_ERR_IO;
	uint32_t thisCRC;
	Bytef *compBuf = NULL;

	FREAD_SAFE(&hdr, sizeof(hdr), dataFd);
	if(hdr.magic != RETRON_DATA_MAGIC)
		BAIL(RETRON_DATA_ERR_CORRUPT);

	// cant operate on files with newer version
	if(hdr.fmtVer > RETRON_DATA_FORMAT_VER)
		BAIL(RETRON_DATA_ERR_VERSION);

	*destBufSize = hdr.origSize;
	*destBuf = (void *)malloc(*destBufSize);
	if(*destBuf == NULL)
		BAIL(RETRON_DATA_ERR_MEM);

	if(hdr.flags & RETRON_DATA_FLG_ZLIB_PACKED)
	{
		compBuf = (Bytef *)malloc(hdr.packedSize);
		uLongf destLen = hdr.origSize;
		if(!compBuf)
			BAIL(RETRON_DATA_ERR_MEM);
		FSEEK_SAFE(dataFd, hdr.dataOffset, SEEK_SET);
		FREAD_SAFE(compBuf, hdr.packedSize, dataFd);
		rv = uncompress((Bytef *)*destBuf, &destLen, compBuf, hdr.packedSize);
		if(rv != Z_OK)
			BAIL(RETRON_DATA_ERR_CORRUPT);
		if(destLen != *destBufSize)
			BAIL(RETRON_DATA_ERR_CORRUPT);
	}
	else
	{
		FSEEK_SAFE(dataFd, hdr.dataOffset, SEEK_SET);
		FREAD_SAFE(*destBuf, *destBufSize, dataFd);
	}

	thisCRC = crc32(0, (const Bytef *)*destBuf, *destBufSize);
	if(thisCRC != hdr.crc32)
	{
		LOGE("datafile crc mismatch: %08X, %08X\n", thisCRC, hdr.crc32);
		BAIL(RETRON_DATA_ERR_CORRUPT);
	}

	rv = RETRON_DATA_SUCCESS;

end:
	if(compBuf)
		free(compBuf);
	LOGI("datafile read rv: %d\n", rv);
	return rv;
}

int cRetronData::read(void **destBuf, int *destBufSize)
{
	FILE *fd;
	int rv = RETRON_DATA_ERR_IO;

	// first try to read from temp file.. if this succeeds then it indicates a power outage or filesystem error on the last save
	fd = fopen(mDataTmpFilename, "rb");
	if(fd)
	{
		LOGE("WARNING: temp datafile left over!\n");
		rv = read(fd, destBuf, destBufSize);
		fclose(fd);
		if(rv == RETRON_DATA_SUCCESS)
			return rv;
	}

	// next try to read from the normal data file
	fd = fopen(mDataFilename, "rb");
	if(!fd)
	{
		LOGE("fopen failed: %s\n", mDataFilename);
		return RETRON_DATA_ERR_IO;
	}
	rv = read(fd, destBuf, destBufSize);
	fclose(fd);
	return rv;
}

int cRetronData::read(const char *destFilename)
{
	void *buffer = NULL;
	int fileSize = 0;
	int rv = read(&buffer, &fileSize);
	if(rv != RETRON_DATA_SUCCESS)
	{
		if(buffer)
			free(buffer);
		return rv;
	}
	rv = ((writeFile(destFilename, buffer, fileSize) == true) ? RETRON_DATA_SUCCESS : RETRON_DATA_ERR_IO);
	free(buffer);
	return rv;
}

bool cRetronData::readFile(const char *filename, void **buffer, int *size)
{
	FILE *fd = fopen(filename, "rb");
	if(!fd)
	{
		LOGE("readFile fopen failed: %d, %d\n", fd, errno);
		return false;
	}
	fseek(fd, 0, SEEK_END);
	*size = ftell(fd);
	fseek(fd, 0, SEEK_SET);
	*buffer = (void *)malloc(*size);
	assert(*buffer != NULL);
	int rv = fread(*buffer, *size, 1, fd);
	fclose(fd);
	if(rv != 1)
	{
		LOGE("readFile fread failed: %d, %d\n", rv, errno);
		free(*buffer);
		return false;
	}

	return true;
}

bool cRetronData::writeFile(const char *filename, void *buffer, int size)
{
	FILE *fd = fopen(filename, "wb+");
	if(!fd)
		return false;
	int rv = fwrite(buffer, size, 1, fd);
	fclose(fd);
	if(rv != 1)
		return false;

	return flushFs(filename);
}

bool cRetronData::flushFs(const char *filename)
{
	sync();
	return true;
}

User avatar
hikaricore
Site Admin
Posts: 171
Joined: May 22nd, 2014, 7:36 am
Location: United States

Re: Saved State Format

Post by hikaricore » January 19th, 2015, 1:47 am

Retron Tèch wrote:Following are the source files used by RetroN 5 for manipulating the save data. Placed into the public domain - use as you desire.

dataFile.h

Code: Select all

#ifndef _DATAFILE_H
#define _DATAFILE_H

#define RETRON_DATA_MAGIC			(0x354E5452)	// "RTN5"
#define RETRON_DATA_FORMAT_VER		(1)
#define RETRON_DATA_TMP_POSTFIX		".TMP"

#define RETRON_DATA_FLG_ZLIB_PACKED	(0x01)

#define RETRON_DATA_DEFCOMPRESS		(true)
#define RETRON_DATA_RAMFILE			"/mnt/ram/tempdata.bin"

enum
{
	RETRON_DATA_SUCCESS = 0,
	RETRON_DATA_ERR_CORRUPT = -1,
	RETRON_DATA_ERR_IO = -2,
	RETRON_DATA_ERR_MEM = -3,
	RETRON_DATA_ERR_VERSION = -4,
	RETRON_DATA_ERR_MISC = -5,
};

typedef struct
{
	uint32_t magic;
	uint16_t fmtVer;
	uint16_t flags;
	uint32_t origSize;
	uint32_t packedSize;
	uint32_t dataOffset;
	uint32_t crc32;
	uint8_t data[0];
} t_retronDataHdr;

class cRetronData
{
public:

	cRetronData(const char *dataFilename);
	~cRetronData();
	int write(void *srcBuf, int srcBufSize, bool compress); // write from buffer into datafile
	int write(const char *srcFilename, bool compress); // write from normal file into datafile
	int read(void **destBuf, int *destBufSize); // read from datafile into buffer
	int read(const char *destFilename); // read from datafile into normal file

private:

	bool readFile(const char *filename, void **buffer, int *size);
	bool writeFile(const char *filename, void *buffer, int size);
	bool flushFs(const char *filename);
	int read(FILE *dataFd, void **destBuf, int *destBufSize);

	const char *mDataFilename, *mDataTmpFilename;
};

#endif
dataFile.cpp

Code: Select all

#include "retronEngine.h"

/*
 * Retron data file safety wrapper - protects data files (such as snapshots, sram states) with CRC32 and ensures FS integrity
 * by first writing to a temp file then switching via rename at the end, and flushing FS after use. Data files are optionally compressed
 */

#define FREAD_SAFE(ptr, bytes, fd)	{ int _n = fread((ptr), 1, (bytes), (fd)); if(_n != (bytes)) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define FWRITE_SAFE(ptr, bytes, fd)	{ int _n = fwrite((ptr), 1, (bytes), (fd)); if(_n != (bytes)) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define FSEEK_SAFE(fd, off, whence)	{ int _o = fseek((fd), (off), (whence)); if(_o < 0) { rv = RETRON_DATA_ERR_IO; goto end; } \
										if(((whence) == SEEK_SET) && (ftell((fd)) != (off))) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define BAIL(code)					{ rv = code; goto end; }

cRetronData::cRetronData(const char *dataFilename)
{
	mDataFilename = strdup(dataFilename);
	assert(dataFilename != NULL);
	int tmpLen = strlen(mDataFilename) + strlen(RETRON_DATA_TMP_POSTFIX) + 1;
	char *tmpName = (char *)malloc(tmpLen);
	assert(tmpName != NULL);
	strcpy(tmpName, (char *)mDataFilename);
	strcat(tmpName, RETRON_DATA_TMP_POSTFIX);
	mDataTmpFilename = tmpName;
}

cRetronData::~cRetronData()
{
	if(mDataFilename)
		free((void *)mDataFilename);
	if(mDataTmpFilename)
		free((void *)mDataTmpFilename);
}

int cRetronData::write(void *srcBuf, int srcBufSize, bool compress)
{
	t_retronDataHdr hdr;
	FILE *fd = NULL;
	int rv = RETRON_DATA_ERR_IO;
	Bytef *destBuf = NULL;

	memset(&hdr, 0, sizeof(hdr));
	hdr.magic = RETRON_DATA_MAGIC;
	hdr.fmtVer = RETRON_DATA_FORMAT_VER;
	hdr.dataOffset = sizeof(hdr);
	hdr.crc32 = crc32(0, (const Bytef *)srcBuf, srcBufSize);
	hdr.origSize = srcBufSize;

	fd = fopen(mDataTmpFilename, "wb+");
	if(!fd)
		BAIL(RETRON_DATA_ERR_IO);

	if(compress)
	{
		hdr.flags |= RETRON_DATA_FORMAT_VER;

		uLongf destLen = compressBound(srcBufSize);
		destBuf = (Bytef *)malloc(destLen);
		if(!destBuf)
			BAIL(RETRON_DATA_ERR_MEM);
		rv = compress2(destBuf, &destLen, (const Bytef *)srcBuf, srcBufSize, Z_DEFAULT_COMPRESSION);
		if(rv != Z_OK)
			BAIL(RETRON_DATA_ERR_MISC);

		hdr.packedSize = destLen;
		FWRITE_SAFE(&hdr, sizeof(hdr), fd);
		FWRITE_SAFE(destBuf, destLen, fd);
	}
	else
	{
		FWRITE_SAFE(&hdr, sizeof(hdr), fd);
		FWRITE_SAFE(srcBuf, srcBufSize, fd);
	}

	fclose(fd);
	fd = NULL;
	if(flushFs(mDataTmpFilename) != true)
		BAIL(RETRON_DATA_ERR_IO);
	// at this point the filesystem should have a clean copy of the datafile at the temp location

	// only try to remove it file already exists
	struct stat st;
	if(lstat(mDataFilename, &st) >= 0)
	{
		if(remove(mDataFilename) < 0)
		{
			LOGI("bail 1 %d\n", rv);
			BAIL(RETRON_DATA_ERR_IO);
		}
	}
	if(rename(mDataTmpFilename, mDataFilename) < 0)
	{
		LOGI("bail 2\n");
		BAIL(RETRON_DATA_ERR_IO);
	}
	if(flushFs(mDataTmpFilename) != true)
		BAIL(RETRON_DATA_ERR_IO);

	// at this point, the old file has been removed and the temp file renamed, and everything flushed - we're done
	rv = RETRON_DATA_SUCCESS;

end:
	if(fd)
		fclose(fd);
	if(destBuf)
		free(destBuf);
	LOGI("datafile write rv: %d\n", rv);
	return rv;
}

// TODO: implement proper streaming code, so we don't need to read the entire file in first
int cRetronData::write(const char *srcFilename, bool compress)
{
	void *buffer = NULL;
	int fileSize = 0;
	if(readFile(srcFilename, &buffer, &fileSize) != true)
	{
		LOGE("retron dat failed to read file\n");
		return RETRON_DATA_ERR_IO;
	}
	int rv = write(buffer, fileSize, compress);
	free(buffer);
	return rv;
}

int cRetronData::read(FILE *dataFd, void **destBuf, int *destBufSize)
{
	t_retronDataHdr hdr;
	int rv = RETRON_DATA_ERR_IO;
	uint32_t thisCRC;
	Bytef *compBuf = NULL;

	FREAD_SAFE(&hdr, sizeof(hdr), dataFd);
	if(hdr.magic != RETRON_DATA_MAGIC)
		BAIL(RETRON_DATA_ERR_CORRUPT);

	// cant operate on files with newer version
	if(hdr.fmtVer > RETRON_DATA_FORMAT_VER)
		BAIL(RETRON_DATA_ERR_VERSION);

	*destBufSize = hdr.origSize;
	*destBuf = (void *)malloc(*destBufSize);
	if(*destBuf == NULL)
		BAIL(RETRON_DATA_ERR_MEM);

	if(hdr.flags & RETRON_DATA_FLG_ZLIB_PACKED)
	{
		compBuf = (Bytef *)malloc(hdr.packedSize);
		uLongf destLen = hdr.origSize;
		if(!compBuf)
			BAIL(RETRON_DATA_ERR_MEM);
		FSEEK_SAFE(dataFd, hdr.dataOffset, SEEK_SET);
		FREAD_SAFE(compBuf, hdr.packedSize, dataFd);
		rv = uncompress((Bytef *)*destBuf, &destLen, compBuf, hdr.packedSize);
		if(rv != Z_OK)
			BAIL(RETRON_DATA_ERR_CORRUPT);
		if(destLen != *destBufSize)
			BAIL(RETRON_DATA_ERR_CORRUPT);
	}
	else
	{
		FSEEK_SAFE(dataFd, hdr.dataOffset, SEEK_SET);
		FREAD_SAFE(*destBuf, *destBufSize, dataFd);
	}

	thisCRC = crc32(0, (const Bytef *)*destBuf, *destBufSize);
	if(thisCRC != hdr.crc32)
	{
		LOGE("datafile crc mismatch: %08X, %08X\n", thisCRC, hdr.crc32);
		BAIL(RETRON_DATA_ERR_CORRUPT);
	}

	rv = RETRON_DATA_SUCCESS;

end:
	if(compBuf)
		free(compBuf);
	LOGI("datafile read rv: %d\n", rv);
	return rv;
}

int cRetronData::read(void **destBuf, int *destBufSize)
{
	FILE *fd;
	int rv = RETRON_DATA_ERR_IO;

	// first try to read from temp file.. if this succeeds then it indicates a power outage or filesystem error on the last save
	fd = fopen(mDataTmpFilename, "rb");
	if(fd)
	{
		LOGE("WARNING: temp datafile left over!\n");
		rv = read(fd, destBuf, destBufSize);
		fclose(fd);
		if(rv == RETRON_DATA_SUCCESS)
			return rv;
	}

	// next try to read from the normal data file
	fd = fopen(mDataFilename, "rb");
	if(!fd)
	{
		LOGE("fopen failed: %s\n", mDataFilename);
		return RETRON_DATA_ERR_IO;
	}
	rv = read(fd, destBuf, destBufSize);
	fclose(fd);
	return rv;
}

int cRetronData::read(const char *destFilename)
{
	void *buffer = NULL;
	int fileSize = 0;
	int rv = read(&buffer, &fileSize);
	if(rv != RETRON_DATA_SUCCESS)
	{
		if(buffer)
			free(buffer);
		return rv;
	}
	rv = ((writeFile(destFilename, buffer, fileSize) == true) ? RETRON_DATA_SUCCESS : RETRON_DATA_ERR_IO);
	free(buffer);
	return rv;
}

bool cRetronData::readFile(const char *filename, void **buffer, int *size)
{
	FILE *fd = fopen(filename, "rb");
	if(!fd)
	{
		LOGE("readFile fopen failed: %d, %d\n", fd, errno);
		return false;
	}
	fseek(fd, 0, SEEK_END);
	*size = ftell(fd);
	fseek(fd, 0, SEEK_SET);
	*buffer = (void *)malloc(*size);
	assert(*buffer != NULL);
	int rv = fread(*buffer, *size, 1, fd);
	fclose(fd);
	if(rv != 1)
	{
		LOGE("readFile fread failed: %d, %d\n", rv, errno);
		free(*buffer);
		return false;
	}

	return true;
}

bool cRetronData::writeFile(const char *filename, void *buffer, int size)
{
	FILE *fd = fopen(filename, "wb+");
	if(!fd)
		return false;
	int rv = fwrite(buffer, size, 1, fd);
	fclose(fd);
	if(rv != 1)
		return false;

	return flushFs(filename);
}

bool cRetronData::flushFs(const char *filename)
{
	sync();
	return true;
}
Fantastic! I suspect this will be of great use. :D

User avatar
1pil
Posts: 67
Joined: December 31st, 2014, 3:15 pm

Re: Saved State Format

Post by 1pil » January 19th, 2015, 1:50 pm

hikaricore wrote:
Retron Tèch wrote:Following are the source files used by RetroN 5 for manipulating the save data. Placed into the public domain - use as you desire.

dataFile.h

Code: Select all

#ifndef _DATAFILE_H
#define _DATAFILE_H

#define RETRON_DATA_MAGIC			(0x354E5452)	// "RTN5"
#define RETRON_DATA_FORMAT_VER		(1)
#define RETRON_DATA_TMP_POSTFIX		".TMP"

#define RETRON_DATA_FLG_ZLIB_PACKED	(0x01)

#define RETRON_DATA_DEFCOMPRESS		(true)
#define RETRON_DATA_RAMFILE			"/mnt/ram/tempdata.bin"

enum
{
	RETRON_DATA_SUCCESS = 0,
	RETRON_DATA_ERR_CORRUPT = -1,
	RETRON_DATA_ERR_IO = -2,
	RETRON_DATA_ERR_MEM = -3,
	RETRON_DATA_ERR_VERSION = -4,
	RETRON_DATA_ERR_MISC = -5,
};

typedef struct
{
	uint32_t magic;
	uint16_t fmtVer;
	uint16_t flags;
	uint32_t origSize;
	uint32_t packedSize;
	uint32_t dataOffset;
	uint32_t crc32;
	uint8_t data[0];
} t_retronDataHdr;

class cRetronData
{
public:

	cRetronData(const char *dataFilename);
	~cRetronData();
	int write(void *srcBuf, int srcBufSize, bool compress); // write from buffer into datafile
	int write(const char *srcFilename, bool compress); // write from normal file into datafile
	int read(void **destBuf, int *destBufSize); // read from datafile into buffer
	int read(const char *destFilename); // read from datafile into normal file

private:

	bool readFile(const char *filename, void **buffer, int *size);
	bool writeFile(const char *filename, void *buffer, int size);
	bool flushFs(const char *filename);
	int read(FILE *dataFd, void **destBuf, int *destBufSize);

	const char *mDataFilename, *mDataTmpFilename;
};

#endif
dataFile.cpp

Code: Select all

#include "retronEngine.h"

/*
 * Retron data file safety wrapper - protects data files (such as snapshots, sram states) with CRC32 and ensures FS integrity
 * by first writing to a temp file then switching via rename at the end, and flushing FS after use. Data files are optionally compressed
 */

#define FREAD_SAFE(ptr, bytes, fd)	{ int _n = fread((ptr), 1, (bytes), (fd)); if(_n != (bytes)) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define FWRITE_SAFE(ptr, bytes, fd)	{ int _n = fwrite((ptr), 1, (bytes), (fd)); if(_n != (bytes)) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define FSEEK_SAFE(fd, off, whence)	{ int _o = fseek((fd), (off), (whence)); if(_o < 0) { rv = RETRON_DATA_ERR_IO; goto end; } \
										if(((whence) == SEEK_SET) && (ftell((fd)) != (off))) { rv = RETRON_DATA_ERR_IO; goto end; } }
#define BAIL(code)					{ rv = code; goto end; }

cRetronData::cRetronData(const char *dataFilename)
{
	mDataFilename = strdup(dataFilename);
	assert(dataFilename != NULL);
	int tmpLen = strlen(mDataFilename) + strlen(RETRON_DATA_TMP_POSTFIX) + 1;
	char *tmpName = (char *)malloc(tmpLen);
	assert(tmpName != NULL);
	strcpy(tmpName, (char *)mDataFilename);
	strcat(tmpName, RETRON_DATA_TMP_POSTFIX);
	mDataTmpFilename = tmpName;
}

cRetronData::~cRetronData()
{
	if(mDataFilename)
		free((void *)mDataFilename);
	if(mDataTmpFilename)
		free((void *)mDataTmpFilename);
}

int cRetronData::write(void *srcBuf, int srcBufSize, bool compress)
{
	t_retronDataHdr hdr;
	FILE *fd = NULL;
	int rv = RETRON_DATA_ERR_IO;
	Bytef *destBuf = NULL;

	memset(&hdr, 0, sizeof(hdr));
	hdr.magic = RETRON_DATA_MAGIC;
	hdr.fmtVer = RETRON_DATA_FORMAT_VER;
	hdr.dataOffset = sizeof(hdr);
	hdr.crc32 = crc32(0, (const Bytef *)srcBuf, srcBufSize);
	hdr.origSize = srcBufSize;

	fd = fopen(mDataTmpFilename, "wb+");
	if(!fd)
		BAIL(RETRON_DATA_ERR_IO);

	if(compress)
	{
		hdr.flags |= RETRON_DATA_FORMAT_VER;

		uLongf destLen = compressBound(srcBufSize);
		destBuf = (Bytef *)malloc(destLen);
		if(!destBuf)
			BAIL(RETRON_DATA_ERR_MEM);
		rv = compress2(destBuf, &destLen, (const Bytef *)srcBuf, srcBufSize, Z_DEFAULT_COMPRESSION);
		if(rv != Z_OK)
			BAIL(RETRON_DATA_ERR_MISC);

		hdr.packedSize = destLen;
		FWRITE_SAFE(&hdr, sizeof(hdr), fd);
		FWRITE_SAFE(destBuf, destLen, fd);
	}
	else
	{
		FWRITE_SAFE(&hdr, sizeof(hdr), fd);
		FWRITE_SAFE(srcBuf, srcBufSize, fd);
	}

	fclose(fd);
	fd = NULL;
	if(flushFs(mDataTmpFilename) != true)
		BAIL(RETRON_DATA_ERR_IO);
	// at this point the filesystem should have a clean copy of the datafile at the temp location

	// only try to remove it file already exists
	struct stat st;
	if(lstat(mDataFilename, &st) >= 0)
	{
		if(remove(mDataFilename) < 0)
		{
			LOGI("bail 1 %d\n", rv);
			BAIL(RETRON_DATA_ERR_IO);
		}
	}
	if(rename(mDataTmpFilename, mDataFilename) < 0)
	{
		LOGI("bail 2\n");
		BAIL(RETRON_DATA_ERR_IO);
	}
	if(flushFs(mDataTmpFilename) != true)
		BAIL(RETRON_DATA_ERR_IO);

	// at this point, the old file has been removed and the temp file renamed, and everything flushed - we're done
	rv = RETRON_DATA_SUCCESS;

end:
	if(fd)
		fclose(fd);
	if(destBuf)
		free(destBuf);
	LOGI("datafile write rv: %d\n", rv);
	return rv;
}

// TODO: implement proper streaming code, so we don't need to read the entire file in first
int cRetronData::write(const char *srcFilename, bool compress)
{
	void *buffer = NULL;
	int fileSize = 0;
	if(readFile(srcFilename, &buffer, &fileSize) != true)
	{
		LOGE("retron dat failed to read file\n");
		return RETRON_DATA_ERR_IO;
	}
	int rv = write(buffer, fileSize, compress);
	free(buffer);
	return rv;
}

int cRetronData::read(FILE *dataFd, void **destBuf, int *destBufSize)
{
	t_retronDataHdr hdr;
	int rv = RETRON_DATA_ERR_IO;
	uint32_t thisCRC;
	Bytef *compBuf = NULL;

	FREAD_SAFE(&hdr, sizeof(hdr), dataFd);
	if(hdr.magic != RETRON_DATA_MAGIC)
		BAIL(RETRON_DATA_ERR_CORRUPT);

	// cant operate on files with newer version
	if(hdr.fmtVer > RETRON_DATA_FORMAT_VER)
		BAIL(RETRON_DATA_ERR_VERSION);

	*destBufSize = hdr.origSize;
	*destBuf = (void *)malloc(*destBufSize);
	if(*destBuf == NULL)
		BAIL(RETRON_DATA_ERR_MEM);

	if(hdr.flags & RETRON_DATA_FLG_ZLIB_PACKED)
	{
		compBuf = (Bytef *)malloc(hdr.packedSize);
		uLongf destLen = hdr.origSize;
		if(!compBuf)
			BAIL(RETRON_DATA_ERR_MEM);
		FSEEK_SAFE(dataFd, hdr.dataOffset, SEEK_SET);
		FREAD_SAFE(compBuf, hdr.packedSize, dataFd);
		rv = uncompress((Bytef *)*destBuf, &destLen, compBuf, hdr.packedSize);
		if(rv != Z_OK)
			BAIL(RETRON_DATA_ERR_CORRUPT);
		if(destLen != *destBufSize)
			BAIL(RETRON_DATA_ERR_CORRUPT);
	}
	else
	{
		FSEEK_SAFE(dataFd, hdr.dataOffset, SEEK_SET);
		FREAD_SAFE(*destBuf, *destBufSize, dataFd);
	}

	thisCRC = crc32(0, (const Bytef *)*destBuf, *destBufSize);
	if(thisCRC != hdr.crc32)
	{
		LOGE("datafile crc mismatch: %08X, %08X\n", thisCRC, hdr.crc32);
		BAIL(RETRON_DATA_ERR_CORRUPT);
	}

	rv = RETRON_DATA_SUCCESS;

end:
	if(compBuf)
		free(compBuf);
	LOGI("datafile read rv: %d\n", rv);
	return rv;
}

int cRetronData::read(void **destBuf, int *destBufSize)
{
	FILE *fd;
	int rv = RETRON_DATA_ERR_IO;

	// first try to read from temp file.. if this succeeds then it indicates a power outage or filesystem error on the last save
	fd = fopen(mDataTmpFilename, "rb");
	if(fd)
	{
		LOGE("WARNING: temp datafile left over!\n");
		rv = read(fd, destBuf, destBufSize);
		fclose(fd);
		if(rv == RETRON_DATA_SUCCESS)
			return rv;
	}

	// next try to read from the normal data file
	fd = fopen(mDataFilename, "rb");
	if(!fd)
	{
		LOGE("fopen failed: %s\n", mDataFilename);
		return RETRON_DATA_ERR_IO;
	}
	rv = read(fd, destBuf, destBufSize);
	fclose(fd);
	return rv;
}

int cRetronData::read(const char *destFilename)
{
	void *buffer = NULL;
	int fileSize = 0;
	int rv = read(&buffer, &fileSize);
	if(rv != RETRON_DATA_SUCCESS)
	{
		if(buffer)
			free(buffer);
		return rv;
	}
	rv = ((writeFile(destFilename, buffer, fileSize) == true) ? RETRON_DATA_SUCCESS : RETRON_DATA_ERR_IO);
	free(buffer);
	return rv;
}

bool cRetronData::readFile(const char *filename, void **buffer, int *size)
{
	FILE *fd = fopen(filename, "rb");
	if(!fd)
	{
		LOGE("readFile fopen failed: %d, %d\n", fd, errno);
		return false;
	}
	fseek(fd, 0, SEEK_END);
	*size = ftell(fd);
	fseek(fd, 0, SEEK_SET);
	*buffer = (void *)malloc(*size);
	assert(*buffer != NULL);
	int rv = fread(*buffer, *size, 1, fd);
	fclose(fd);
	if(rv != 1)
	{
		LOGE("readFile fread failed: %d, %d\n", rv, errno);
		free(*buffer);
		return false;
	}

	return true;
}

bool cRetronData::writeFile(const char *filename, void *buffer, int size)
{
	FILE *fd = fopen(filename, "wb+");
	if(!fd)
		return false;
	int rv = fwrite(buffer, size, 1, fd);
	fclose(fd);
	if(rv != 1)
		return false;

	return flushFs(filename);
}

bool cRetronData::flushFs(const char *filename)
{
	sync();
	return true;
}
Fantastic! I suspect this will be of great use. :D
For someone that can understand it I hope. :?

Dan50
Posts: 1
Joined: April 18th, 2015, 10:30 am

Re: Saved State Format

Post by Dan50 » April 18th, 2015, 10:32 am

I would love to have my Retron 5 save work on my Ouya for LeafGreen.

Quarnz
Posts: 3
Joined: June 10th, 2015, 3:49 am

Re: Saved State Format

Post by Quarnz » June 11th, 2015, 8:31 am

Has anyone actually got this to work? I'd be curious to try it myself.

Woff
Posts: 9
Joined: June 23rd, 2015, 6:28 pm

Re: Saved State Format

Post by Woff » June 24th, 2015, 3:49 am

Here is a simple program to extract and pack files from the Retron 5.
https://www.dropbox.com/s/me4pgfy0daas0 ... r.zip?dl=0

Installation notes:
This program requires Microsoft ASP .NET MVC 4 Runtime
https://www.microsoft.com/en-US/downloa ... x?id=17718
and Microsoft Visual C++ 2010 Redistributable.
https://www.microsoft.com/en-US/downloa ... px?id=8328

How to extract a file from a Retron sav file:
  • Drag and drop a file on the windows form or select it by using the dialog box e.g. 'C:\Retron 5\The Legend of Zelda - A Link to the Past (Europe).sav'.
  • Select the output file by using the dialog box e.g. 'C:\Retron 5\The Legend of Zelda - A Link to the Past (Europe).srm'.
  • Start the extraction by pressing 'Read from Retron 5 file'
Note: You must select the correct file suffix for the output file (.srm for SNES, .sav for NES, GBC and GBA, etc.)

How to pack a file to the Retron file format
  • Drag and drop a file on the windows form or select it by using the dialog box e.g. 'C:\Retron 5\The Legend of Zelda - A Link to the Past (Europe).srm'.
  • Start the packing process by pressing 'Write as Retron 5 file'
Note: When packing a file the output file is always called 'inputfilename.sav' e.g. 'C:\Retron 5\The Legend of Zelda - A Link to the Past (Europe).srm.sav'

Woff
Posts: 9
Joined: June 23rd, 2015, 6:28 pm

Re: Saved State Format

Post by Woff » July 2nd, 2015, 6:19 pm

Has anyone tried this yet? Some feedback would be nice. Does it run and does it work? I have converted some files on my Retron and it worked nicely. But it would be great to have some more user experiences.

User avatar
1pil
Posts: 67
Joined: December 31st, 2014, 3:15 pm

Re: Saved State Format

Post by 1pil » August 20th, 2015, 12:28 pm

Gonna try it out.

EDIT3-----oh wait I think I feel really stupid.....brb gonna test it one more time.

User avatar
1pil
Posts: 67
Joined: December 31st, 2014, 3:15 pm

Re: Saved State Format

Post by 1pil » August 21st, 2015, 1:34 pm

It does work. Sorry for the late follow up on me testing. Wasn't able to post again yesterday. But ya it actually worked. I got no issues....when it comes to certain codes or using save editors to edit our save files....this tool is it. It's brilliant. And whats even better is I dont even own the game that I used the save file for, I was using my Dragon Quest VI cartridge and using a ips patch game which was Star Ocean. And the save file as many of you know saves the file with the name of the cartridge and not the ips game your playing. So but the save file actually still works, I had to rename it to my star ocean rom file though when loading it in zsnes emu. But ya the whole process is the same but when editing saves from a ips game you just need to rename it to the game you got for your emulator. And make sure its the exact same rom or else you may have some major issues.

PS. I'd like to thank you Woff for making that editor. You're astounding.

Dahamburgler
Posts: 1
Joined: December 15th, 2015, 2:06 am

Re: Saved State Format

Post by Dahamburgler » December 15th, 2015, 2:11 am

Hello, I have a save file for Final Fantasy (I apparently deleted mine because I thought it had saved my state when I reset the cartridge on my retron, but it didn't). I have a file that's near where I was in the game and want to transfer it to the retron. I used the program posted here and it output a file. I then renamed it to match the Final Fantasy Save (i.e. Final Fantasy (USA).save), but it says "Failed to load SRAM" on start up and seems to then load from the cartridge save (which is blank).

Any help? I'd really like to get back to playing.

Thank you in advance.

Post Reply

Who is online

Users browsing this forum: No registered users and 2 guests