Sign in to follow this  
Black Knight

Save/Load System

Recommended Posts

I did some searching on the forum for this and the only thing people mention is to do some kind of versioning. How can I implement a save/load system so that I can load old saved maps.Currently I have a world class which has a map class member.There is no data in the world class to be saved the map class is the class that I want to save - restore. Here is the world class :
class World
{
public:
    World(DAEnginePtr engine);
    ~World();
    
        
	//
	//	Loads a map from the disk to the Map member
	//
	void				loadMap(DARendererPtr renderer,const std::string& filename);
	//
	//	Saves the map member to the disk
	//
	void				saveMap(const std::string& filename)const;

	//
	//	Frees all D3DPOOL_DEFAULT resources
	//  Calls onLostDevice on member objects
	//
	void				onLostDevice();

	//
	// After lost device create resources again
	// Calls onResetDevice on member objects
	//
	void				onResetDevice(DARenderer* renderer);

	//
	// Update world
	// Calls map update
	//
    void				update(DARenderer* renderer,float elapsedTime);

   
	
	void setMap(boost::shared_ptr<Map> map){m_Map = map;}
	boost::shared_ptr<Map> getMap(){return m_Map;}
	
private:
	
	/*
	 *	Current map
	 */
	boost::shared_ptr<Map> m_Map;
	
	
    /*
     *	Loads the shaders used by the game and the editor
     */
	void				loadShaders(DAEngine* engine);
	void				loadTextures(DAEngine* engine);
	void				loadModels(DAEngine* engine);

};



And the map class :
class Map
{
public:
    // Creates the map according to the tMapCreateParams
	Map(boost::shared_ptr<DARenderer> renderer,const tMapCreateParams* params);
	
	// Creates the map from a file on the disk
	Map(DARendererPtr renderer,const std::string& filename);
   
	//destructor
	~Map();
   	
	
	//
	// Save this map object to file
	//
	void save(const std::string& filename);

	//
	// Save all entities to file                                                                     
	//
	void saveEntities(std::ofstream& file);
    
    // Updates game objects,detect visibilities
	void update(DARenderer* renderer,float elapsedTime);

	
	// Processes collisions between game objects(camera-terrain etc.)
	void processCollisions(DARenderer* renderer,float elapsedTime);

    //Places forest on the map.Randomly selects forestCount locations 
	//and puts treeCount trees around that location
	void placeForest(boost::shared_ptr<DARenderer> renderer,
					 const STMath::Vector3& forestPosition,
					 UINT treeCount,UINT forestRadius,
					 STWin::ProgressBar *progressBar);
	/*
	 *	Changes the sealevel of the map
	 */
	void changeSeaLevel(float newSeaLevel);

	/*
	 *	Called when the d3d device is lost 
	 *	frees D3DPOOL_DEFAULT resources
	 */
	void onLostDevice();
	/*
	 *	Called when the d3d devices is reset
	 *  recreates D3DPOOL_DEFAULT resources
	 */
	void onResetDevice(DARenderer* renderer);

	//Entity		
	void				addEntity(boost::shared_ptr<Entity> addEntity);
	void				deleteEntity(const Entity* entity);
	boost::shared_ptr<Entity> getEntity(int entity)const {return m_Entities[entity];}
	boost::shared_ptr<Entity> getVisibleEntity(int entity)const {return m_VisibleEntities[entity];}
	size_t				getEntityCount()const {return m_Entities.size();}
	size_t				getVisibleEntityCount() const{return m_VisibleEntities.size();}

    //Data Access
    //set/get Name
	void		setName(const std::string& name){m_Name = name;}
	const std::string&  getName()const{return m_Name;}
	
	  
    
    boost::shared_ptr<GeoMipmapTerrain> getTerrain(){return m_Terrain;}
	boost::shared_ptr<Water> getWater(){return m_Water;}
	boost::shared_ptr<SkyPlane> getSkyPlane(){return m_SkyPlane;}

	//static data
	static const float MorningColor[4];
protected:
    //
    // name of the map appearing ingame
	//
    std::string			m_Name;	
    
	//
	// the terrain of this map
	//
	boost::shared_ptr<GeoMipmapTerrain> m_Terrain;

	//
	// The water of the map(null if map does not have water)
	// 
	boost::shared_ptr<Water> m_Water;

	//
	// The skyplane over the map
	//
	boost::shared_ptr<SkyPlane> m_SkyPlane;

	//
	// Entities on the map
	//
	std::vector<boost::shared_ptr<Entity>> m_Entities;

	//
	// visible entities.(filled inside Map::update)
	//
	std::vector<boost::shared_ptr<Entity>> m_VisibleEntities;

};



As you can see the map class holds the other objects and they will need to get saved too. Anyway first I was thinking of giving each class a save load method or make another contructor for the class which constructs the object from a file or data stream.But I don't know how to handle old save games in this approach. So I thought I could write a SaveLoadManager class which saves loads the world. This class can have different save/load method pairs for different versions of save files.For example :
class SaveLoadManager
{
public:

void load(World* world,const std::string& filename);
void save(World* world,const std::string& filename,int version);

void save_version_1(World* world,const std::string& filename);
void load_version_1(World* world,const std::string& filename);

void save_version_2(World* world,const std::string& filename);
void load_version_2(World* world,const std::string& filename);
};

//implementation
void SaveLoadManager::load(World* world,const std::string& filename)
{
 get version from file
 call the appropriate load_version_*() function
}
void SaveLoadManager::save(World* world,const std::string& filename,int version)
{
write version number to file
call the appropriate save_version_*() function
}




Each time I change map,world,terrain I would need a new save_version_* and a save_version_* to save load that new states.Also I would need to write the save file version to the file and call the appropriate load_version_* function. Another problem with this is once I saved some maps with a save/load pair I must not change them again or the maps saved before will be broken. This looks like a very clumsy way of saving loading.So what can I do fellows? Edit:Also this will create lots of code duplication in the save_version_* load_version_* methods. :|

Share this post


Link to post
Share on other sites
What about using text-based save files? If you use something like XML, you can easily ignore information you're not interested in (E.g. if you add a field in version 3, and you're loading version 1, then there'll be information for versions 2 and 3 you're not interested in).

Alternatively, if you really want to use a binary format, you could do what Win32 does - each struct has a size value as it's first member. If the version changes, you increase that size and add new members at the end of the struct.

Older code will ignore the new struct members, and should use the size member to skip the correct number of bytes, and newer code can use the new members without problem.

Share this post


Link to post
Share on other sites
Basically what Evil Steve said, but instead of using sizes (which can be unreliable), use unique ID's. If you ever need to remove data, you deprecate the old ID's without reusing them, and new data just uses new ID's.

Share this post


Link to post
Share on other sites
Quote:
Original post by Evil Steve
Alternatively, if you really want to use a binary format, you could do what Win32 does - each struct has a size value as it's first member. If the version changes, you increase that size and add new members at the end of the struct.

Older code will ignore the new struct members, and should use the size member to skip the correct number of bytes, and newer code can use the new members without problem.

Hmm this sound promising ill give it a try.

Share this post


Link to post
Share on other sites
Quote:
Original post by Zipster
Basically what Evil Steve said, but instead of using sizes (which can be unreliable), use unique ID's. If you ever need to remove data, you deprecate the old ID's without reusing them, and new data just uses new ID's.


What do you mean using IDs instead of sizes?
How would i know how much data to skip when loading an old file for example

Share this post


Link to post
Share on other sites
I assume Zipster means giving each struct an ID number, and then when you load the save file you'll load a number of structs with IDs. Your code then just needs to pull out the structs with particular IDs. Old code would see unrecognised IDs but ignore them, and new code can load the older IDs and the newer ones too.

Share this post


Link to post
Share on other sites
I still don't see how ids will work.
I would give my map,terrain,sky plane classes unique ids and write that ids to file with the classes?
Then later on when I add new data members to lets say terrain I will change the unique ID?
And when loading if i get a old id for the terrain ill use old loading code and if i get a new ID i will use new loading code?
Lol im confused.
Another thing is I can't do just out.write(size,terrain) so ill basically load/save each member one by one or ill create save/load structs which will contain only the data that will be saved for each class.
Then save that struct in one write call.

Share this post


Link to post
Share on other sites
Quote:
Original post by Black Knight
I still don't see how ids will work.
I would give my map,terrain,sky plane classes unique ids and write that ids to file with the classes?
Then later on when I add new data members to lets say terrain I will change the unique ID?
And when loading if i get a old id for the terrain ill use old loading code and if i get a new ID i will use new loading code?
Lol im confused.
Another thing is I can't do just out.write(size,terrain) so ill basically load/save each member one by one or ill create save/load structs which will contain only the data that will be saved for each class.
Then save that struct in one write call.
Something like this:

struct DataChunk
{
DataChunk() {}
DataChunk(unsigned int nChunkID, unsigned int nChunkSize) :
nID(nChunkID), nSize(nChunkSize) {}

unsigned int nID;
unsigned int nSize;
};

struct WorldData : public DataChunk
{
WorldData() : DataChunk('WRD1', sizeof(WorldData)) {}

int nWidth;
int nHeight;
// Any other data members here
};


When you load your save file, you'd read in sizeof(DataChunk) bytes, which will tell you the size and ID of the chunk you're about to read. Then you read in that many bytes (minus sizeof(DataChunk)). That gives you a buffer containing the chunk information.
Once you've got that, you can store the chunk in memory and read all the other chunks. And once that's done, you can get the rest of your code to parse the list of read chunks and grab the ones it needs (Or you could have your system notify the relevant classes that you've just read a chunk).

The point is that all your data is wrapped up in these structs, and is accessed by a unique ID. When you change a struct, you also give it a new ID so that older code doesn't try to read this chunk which is not in the same format it's expecting.


However - do you really need to support multiple save versions?

Share this post


Link to post
Share on other sites
Well at the moment my world editor can create modify terrains,place entities rotate scale them.Paint the terrain with different textures etc.
I want to be able to save the maps so that I can load them in the game and use them.
So when I spend some time and create a map in the editor now I don't want to lose all the placed entities the terrain modifications later when I add more data to one of the classes.This is why Im trying to be able to load old saved maps.

In the example structures you posted,I will need to convert my map,world,terrain classes to these POD structs because my classes have pointers.Thats another pain too.


Share this post


Link to post
Share on other sites
Quote:
In the example structures you posted,I will need to convert my map,world,terrain classes to these POD structs because my classes have pointers.Thats another pain too.


Well, yes, but you have to do that anyway.

Have you read this yet?

Share this post


Link to post
Share on other sites
I haven't.
My situation goes into :
"when the objects to be serialized contain pointers to other objects, but when those pointers form a tree with no cycles and no joins. "
He talks about constructing objects recursively in the constructors etc.
Its all good but I still need to figure out which way to go with different file versions.




Share this post


Link to post
Share on other sites
Quote:
Original post by Evil Steve
I assume Zipster means giving each struct an ID number, and then when you load the save file you'll load a number of structs with IDs. Your code then just needs to pull out the structs with particular IDs. Old code would see unrecognised IDs but ignore them, and new code can load the older IDs and the newer ones too.

Yes, however I would take it a step further and give each individual field a unique ID. Otherwise you end up maintaining a bunch of different structs for each version, with each one containing mostly identical members save for the one or two you added/removed each version.

Share this post


Link to post
Share on other sites
I recommend using the Bridge pattern in your case, Black Knight. What you are after at the moment is having different loading schemas (per version) requiring latest output in terms of structs, lists etc. So, you can separate implementation from interface for those classes that have changed from version to version and instantiate implementation for those classes based on the version present in the file/memory. A problem still stands where you convert/type-cast interfaces/abstract classes returned after loading. An example would be something like this:


class CMap
{
// ...
void loadMap(DARendererPtr renderer,const std::string& filename)
{
// open file into pFile

int iVersion = CURRENT_VERSION;
fread(&iVersion, sizeof(int), 1, pFile);
CMapImpl *pMapImpl = CMapImplFactory::GetSingleton().Create(iVersion);
pMapImpl->loadMap(this, pFile);
}
};



Essentially, it is the same as writing function per version of the class, however this one is more elegant and easier to organize.
Alternatively, you can try using boost' serialization library which allows just for the thing with different class versions. (See http://www.boost.org/doc/libs/release/libs/serialization)

Share this post


Link to post
Share on other sites
Quote:
Original post by Zipster
Quote:
Original post by Evil Steve
I assume Zipster means giving each struct an ID number, and then when you load the save file you'll load a number of structs with IDs. Your code then just needs to pull out the structs with particular IDs. Old code would see unrecognised IDs but ignore them, and new code can load the older IDs and the newer ones too.

Yes, however I would take it a step further and give each individual field a unique ID. Otherwise you end up maintaining a bunch of different structs for each version, with each one containing mostly identical members save for the one or two you added/removed each version.


How would I go about giving each field a unique ID?
struct MapData
{
std::map<int,Vector3> position;
std::map<int,float> waterLevel;
};

Can you give a small example which writes a small structure like that?
Because I don't understand how to use the IDs to read data.It seems that I have to write the ID to the file then the data and when reading I need to read the ID and check it agains something but I dont get what.
And how am I going to manage all the unique IDs.

Share this post


Link to post
Share on other sites
Well for now ill keep it simple and just write some structures to file :

struct TerrainDataChunk
{
int size;
int scale;
int patchSize;
boost::shared_array<float> heightData;
boost::shared_array<unsigned char> alphaData;
};
struct WaterDataChunk
{
int size;
int scale;
STMath::Vector3 position;
};
struct SkyPlaneDataChunk
{
int size;
int scale;
STMath::Vector3 position;
float atmosphereRadius;
float tiling;
float cloudMove;
float currentColor[4];
};

struct MapDataChunk
{
std::string name;
};



These are enough to recreate the game objects.I'll read these from the file and then send the data to the constructors of the objects to create them again.
Anyway when I was playing with structures and file a few years ago writing a structure to a file was giving different file size then the size of the structure and when reading back everything was getting back wrong.
Someone told me to use pragma pack(1) or something like that so the structures are fit to 1 byte boundries.
Can someone explain if I reall need it and why?

Share this post


Link to post
Share on other sites
Only the save/load code needs to know about the IDs. The actual data and structures should be completely oblivious to it. Here's some Pseudo-code. There are a few classes I omit implementation for because it should be clear what they do:

struct MyStructType
{
int myInt;
float myFloat;
OtherPODType myPOD;

void Save(OStream* stream) const;
void Load(IStream* stream);
};

enum {
MYSTRUCT_INT_ID = 0x100,
MYSTRUCT_FLOAT_ID,
MYSTRUCT_OTHER_POD_ID,
/* !!!NEW IDS ALWAYS GO HERE, NEVER CHANGE THE ONES ABOVE AFTER THEY'RE IN USE!!! */
};

void MyStructType::Save(OStream* stream) const
{
stream->WriteIDAndData(MYSTRUCT_INT_ID, myInt);
stream->WriteIDAndData(MYSTRUCT_FLOAT_ID, myFloat);
stream->WriteIDAndData(MYSTRUCT_OTHER_POD_ID, myPOD);
}

/* Here's where the magic happens */
void MyStructType::Load(IStream* stream)
{
int id = stream->ReadID();
while(!stream->IsAtEnd())
{
switch(id)
{
case MYSTRUCT_INT_ID:
stream->ReadData(myInt);
break;
case MYSTRUCT_FLOAT_ID:
stream->ReadData(myFloat);
break;
case MYSTRUCT_OTHER_POD_ID:
stream->ReadData(myPOD);
break;
default:
Log("Unrecognized ID: %d", id);
if(!SaveLoadCompatabilityLayer::HandleOldData(id, stream))
stream->SkipData();
break;
}

id = stream->ReadID();
}
}


From there you can easily add new IDs, and add/remove sections of the code that save or load certain fields. If a field exists then the switch statement catches it. If the ID isn't recognized then that field it might have been removed in a newer version and you can either ignore it or run special code that handles any backwards compatibility. And finally if one of your case blocks doesn't hit (because you're trying to load a new field that doesn't exist in the old data), you can easily add flags to check what was loaded and initialize any un-loaded data to appropriate default values.

Share this post


Link to post
Share on other sites
Hmm how should I nest structures with this?
Here is how my code looks like at the moment :

struct TerrainDataChunk
{
struct TerrainInfo
{
int size;
int scale;
int patchSize;
int alphaMapSize;
};
TerrainInfo info;
boost::shared_array<float> heightData;
boost::shared_array<unsigned char> alphaMapData;

void save(std::ofstream& file)const;
void load(std::ifstream& file);
};
namespace TerrainDataChunkIDs
{
enum types
{

};
}



my other structures are simple they only contain ints floats etc.
But the terrain structure has arrays and another structure inside.
And I guess this method will be slower then just reading the whole struct with one read as its reading all the fields one by one from the file right?

Share this post


Link to post
Share on other sites
I want to ask something about this code :


void MyStructType::Load(IStream* stream)
{
int id = stream->ReadID();
while(!stream->IsAtEnd())
{
switch(id)
{
case MYSTRUCT_INT_ID:
stream->ReadData(myInt);
break;
case MYSTRUCT_FLOAT_ID:
stream->ReadData(myFloat);
break;
case MYSTRUCT_OTHER_POD_ID:
stream->ReadData(myPOD);
break;
default:
Log("Unrecognized ID: %d", id);
if(!SaveLoadCompatabilityLayer::HandleOldData(id, stream))
stream->SkipData();
break;
}

id = stream->ReadID();
}
}







You are looping until the stream ends so are you opening the file again for each structure ?Because this would only read one structure and read lots of other ids from the file for other structures which are useless for this structure.

Im going for something like this :


void MapDataChunk::load(std::ifstream &file)
{
unsigned int currentID = 0;
//read the first ID from the file
file.read((char*)¤tID,4);

while(!file.eof())
{
switch(currentID)
{
//read the map name
case MapDataChunkIDs::NAME_ID:
name = STUtil::ReadStringFromStream(file);
break;
case MapDataChunkIDs::TERRAIN_ID:
terrainData.load(file);
break;
case MapDataChunkIDs::SKYPLANE_ID:
skyPlaneData.load(file);
break;
case MapDataChunkIDs::WATER_ID:
waterData.load(file);
break;

default:

break;

}
file.read((char*)¤tID,4);

}

}




But inside the terrainData.load I don't know how to handle the switch because I need a second loop there to read the fields and data.

If i stick everything into one structure it might work :

struct MapDataChunkAll
{
//map
std::string map_name;

//terrain
int terrain_size;
int terrain_scale;
int terrain_patchSize;
int terrain_alphaMapSize;
boost::shared_array<float> terrain_heightData;
boost::shared_array<unsigned char> terrain_alphaMapData;

//water
int water_size;
int water_scale;
STMath::Vector3 water_position;

//sky
int skyPlane_size;
int skyPlane_scale;
STMath::Vector3 skyPlane_position;
float skyPlane_atmosphereRadius;
float skyPlane_tiling;
float skyPlane_cloudMove;
float currentColor[4];

}

namespace MapDataChunkIDs
{
enum
{
NAME_ID = 0x100,
TERRAIN_SIZE_ID,
TERRAIN_SCALE_ID,
TERRAIN_PATCHSIZE_ID,
TERRAIN_ALPHAMAPSIZE_ID,
...

};
}



[Edited by - Black Knight on July 30, 2008 11:26:38 AM]

Share this post


Link to post
Share on other sites
Quote:
Original post by Black Knight
I want to ask something about this code :
*** Source Snippet Removed ***

You are looping until the stream ends so are you opening the file again for each structure ?Because this would only read one structure and read lots of other ids from the file for other structures which are useless for this structure.

Yeah I realized this after I had already gone to bed, but you don't really want to loop until the end of stream, only until the end of the block of data for the current object. Since this type of save/load system tends to be very hierarchical, it should be easy to place markers in the file that indicate the beginning and end of objects, for instance by using stream->WriteBeginObject() and stream->WriteEndObject() functions around the calls to write data. At the end of the day your file structure should resemble a tree, where each object can save/load a bunch of sub-objects, where leaves are your raw data.

So your terrain loading code would work the same as your map loading code. The only challenge is writing a serialization system that handles hierarchical storage, but that's not too difficult if you're familiar with trees.

Share this post


Link to post
Share on other sites
Quote:
Original post by Black Knight
I want to ask something about this code :
*** Source Snippet Removed ***

You are looping until the stream ends so are you opening the file again for each structure?


Of course not; there is nothing to close the stream, so it stays open.

Quote:
Because this would only read one structure and read lots of other ids from the file for other structures which are useless for this structure.


Each time through the loop, it would read an id, and then check the id, and then read the corresponding structure. What's the problem?

Quote:
Im going for something like this :
*** Source Snippet Removed ***


Don't do that. Do this:


void MapDataChunk::load(std::ifstream &file)
{
unsigned int currentID = 0;
// Notice: Use the reading operation directly as the condition for the
// loop. The standard library is specially designed to make this work,
// and it's a standard idiom of the language.
while (file.read((char*)&currentID, 4))
{
switch(currentID)
{
// You almost certainly don't want to just have one data member
// of each chunk type. Instead, have a function that creates
// a new chunk, and append it to a container.
case MapDataChunkIDs::NAME_ID:
name = STUtil::ReadStringFromStream(file);
break;
case MapDataChunkIDs::TERRAIN_ID:
terrainData.load(file);
break;
case MapDataChunkIDs::SKYPLANE_ID:
skyPlaneData.load(file);
break;
case MapDataChunkIDs::WATER_ID:
waterData.load(file);
break;
default:
// You really should have better error handling :)
break;

}
}
}



Quote:

But inside the terrainData.load I don't know how to handle the switch because I need a second loop there to read the fields and data.


Why would there be a switch inside the terrainData.load() function? But anyway, you don't have to do anything special to "handle" anything here. Just pass the stream as you're doing. The called function will start reading at the point where the main load function left off, read one chunk's worth of data, and return; then the main function will automatically start reading where the called function left off (this has to do with the stream being passed by reference, and with how streams work in general), and thus read the next ID. So within the called function, you just read each member of the chunk object, one at a time.

Your file is being assumed to contain a sequence of chunks prefaced by IDs. Is that not the case? If it is, are you sure you actually understand what a stream is?

If i stick everything into one structure it might work :
*** Source Snippet Removed ***[/quote]

Share this post


Link to post
Share on other sites
If I keep different structures in the mapdatachunk :

struct MapData
{
std::string name;
TerrainData terrainData;
WaterData waterData;
SkyData skyData;
};


Then I give each of these structure members an ID.MapDataChunkIDs::TERRAIN_ID for the terrain for example.When I read this ID from the stream it indicates the beginning of the terrain data chunk.The TerrainData structure also has members and they all have other IDs so I need a switch for them too,to enable loading old save files.
That was the reason I was thinking of one loop for each structure,because each structure may change in the future.

Quote:

Each time through the loop, it would read an id, and then check the id, and then read the corresponding structure. What's the problem?

Each structure member has also different IDs. ^^
Quote:

Your file is being assumed to contain a sequence of chunks prefaced by IDs. Is that not the case? If it is, are you sure you actually understand what a stream is?

Actually it contains a sequence of fields(ints,floats,pointers to data) each prefaced by IDs.I think I know what a stream is but you make me doubt myself [smile]

Share this post


Link to post
Share on other sites
SaveLoadManager.h

#ifndef INC_SAVELOADMNGR_H
#define INC_SAVELOADMNGR_H

#include <string>
#include "DARenderer.h"
#include "Math/Vector3.h"
#include <boost/shared_array.hpp>

//forward declaration
class World;



struct MapDataChunkAll
{
MapDataChunkAll():
map_name(""),
terrain_size(0),
terrain_scale(0),
terrain_patchSize(0),
terrain_alphaMapSize(0),
terrain_heightData(0),
terrain_alphaMapData(0),

water_size(0),
water_scale(0),
water_position(0.0f,0.0f,0.0f),

skyPlane_size(0),
skyPlane_scale(0),
skyPlane_position(0.0f,0.0f,0.0f),
skyPlane_atmosphereRadius(0.0f),
skyPlane_tiling(0.0f),
skyPlane_cloudMove(0.0f)

{
std::fill(skyPlane_currentColor,skyPlane_currentColor+4,0.0f);

}
//map
std::string map_name;

//terrain
int terrain_size;
int terrain_scale;
int terrain_patchSize;
int terrain_alphaMapSize;
boost::shared_array<float> terrain_heightData;
boost::shared_array<unsigned char> terrain_alphaMapData;

//water
int water_size;
int water_scale;
STMath::Vector3 water_position;

//sky
int skyPlane_size;
int skyPlane_scale;
STMath::Vector3 skyPlane_position;
float skyPlane_atmosphereRadius;
float skyPlane_tiling;
float skyPlane_cloudMove;
float skyPlane_currentColor[4];

void save(std::ofstream& file)const;
void load(std::ifstream& file);
void writeIDAndData(std::ofstream& file,unsigned int ID,const char* data,int size)const;
void handleOldData(unsigned int ID,std::ifstream& file);


};

namespace MapDataChunkIDs
{
enum
{
MAP_NAME_ID = 0x100,
TERRAIN_SIZE_ID,
TERRAIN_SCALE_ID,
TERRAIN_PATCHSIZE_ID,
TERRAIN_ALPHAMAPSIZE_ID,
TERRAIN_HEIGHTDATA_ID,
TERRAIN_ALPHAMAPDATA_ID,

WATER_SIZE_ID,
WATER_SCALE_ID,
WATER_POSITION_ID,

SKYPLANE_SIZE_ID,
SKYPLANE_SCALE_ID,
SKYPLANE_POSITION_ID,
SKYPLANE_ATMOSPHERE_ID,
SKYPLANE_TILING_ID,
SKYPLANE_CLOUDMOVE_ID,
SKYPLANE_CURRENTCOLOR_ID
};
}

//
// This class saves and loads the world data
//
class SaveLoadMngr
{
public:
SaveLoadMngr(){};


void save(const std::string& filename,World* world);
void load(const std::string& filename,World* world,DARendererPtr renderer);


private:
//save load mngr cannot be copied
SaveLoadMngr(const SaveLoadMngr& );
SaveLoadMngr& operator=(const SaveLoadMngr& );

static const int SIGNATURE = 0x1983;


};


#endif //INC_SAVELOADMNGR_H


SaveLoadManager.cpp



#include "SaveLoadMngr.h"
#include "World.h"
#include "SkyPlane.h"
#include "Water.h"
#include "STUtil.h"

using STE::STUtil;

void SaveLoadMngr::save( const std::string& filename,World* world )
{
//get the map
boost::shared_ptr<Map> map = world->getMap();

//open file
std::ofstream file(filename.c_str(),std::ios_base::binary);

//every dark age map has this signature at the beginning
file.write((const char*)&SIGNATURE,4);

//fill the map data structure
MapDataChunkAll mapData;

//fill the terrain
boost::shared_ptr<const GeoMipmapTerrain> terrain = map->getTerrain();
mapData.map_name = map->getName();
mapData.terrain_size = terrain->getSize();
mapData.terrain_scale = terrain->getScale();
mapData.terrain_patchSize = terrain->getPatchSize();
mapData.terrain_alphaMapSize = terrain->getAlphaMapSize();
mapData.terrain_heightData.reset(new float[mapData.terrain_size * mapData.terrain_size]);
mapData.terrain_alphaMapData.reset(new unsigned char[mapData.terrain_alphaMapSize*mapData.terrain_alphaMapSize*4]);

const GeoMipmapTerrain::VertexBlendData* alphaData = terrain->getAlphaMapData();

int index = 0;
int index2 = 0;
//save the alpha data in our structure
for(int i=0; i<mapData.terrain_alphaMapSize; ++i)
for(int k=0; k<mapData.terrain_alphaMapSize; ++k){
mapData.terrain_alphaMapData[index] = alphaData[index2].b;
mapData.terrain_alphaMapData[index+1] = alphaData[index2].g;
mapData.terrain_alphaMapData[index+2] = alphaData[index2].r;
mapData.terrain_alphaMapData[index+3] = alphaData[index2].a;
index+=4;
++index2;

}
const tTerrainVertex* vertices = terrain->getVertices();
index = 0;
//save the vertex heights in our structure
for(int i=0; i<mapData.terrain_size; ++i){
for(int k=0; k<mapData.terrain_size; ++k){
mapData.terrain_heightData[index] = vertices[index].v.y;
++index;
}
}

//fill the sky
boost::shared_ptr<const SkyPlane> skyPlane = map->getSkyPlane();
mapData.skyPlane_atmosphereRadius = skyPlane->getAtmosphereRadius();
mapData.skyPlane_cloudMove = skyPlane->getCloudMove();
mapData.skyPlane_position = skyPlane->getPosition();
mapData.skyPlane_scale = skyPlane->getScale();
mapData.skyPlane_size = skyPlane->getSize();
mapData.skyPlane_tiling = skyPlane->getTiling();
mapData.skyPlane_currentColor[0] = skyPlane->getCurrentColor()->r;
mapData.skyPlane_currentColor[1] = skyPlane->getCurrentColor()->g;
mapData.skyPlane_currentColor[2] = skyPlane->getCurrentColor()->b;
mapData.skyPlane_currentColor[3] = skyPlane->getCurrentColor()->a;

//fill the water
boost::shared_ptr<const Water> water = map->getWater();
mapData.water_position = water->getPosition();
mapData.water_scale = water->getScale();
mapData.water_size = water->getSize();
//save everything to file
mapData.save(file);

}


void SaveLoadMngr::load( const std::string& filename,World* world,DARendererPtr renderer )
{
std::ifstream file(filename.c_str(),std::ios_base::binary);

int signature = 0;
file.read((char*)&signature,4);

if(signature!=SIGNATURE)
throw STE::RunTimeException("Invalid map file",FUNCTIONINFO);


MapDataChunkAll mapData;
mapData.load(file);

// when we reach here the structure is loaded with the data
// need to create the objects and set them to the world now
// If we loaded an old save file there might me some members in the
// map data that are not set we need to check them here and set them to
// defaults or set them to defaults in the MadDataChunkAll constructor.
tTerrainCreateParams createParams ={0};
createParams.patchSize = mapData.terrain_patchSize;
createParams.scale = mapData.terrain_scale;
createParams.size = mapData.terrain_size;
createParams.waterHeight = mapData.water_position.y;
createParams.alphaMapSize = mapData.terrain_alphaMapSize;

boost::shared_ptr<GeoMipmapTerrain> terrain(
new GeoMipmapTerrain(renderer,
&createParams,
mapData.terrain_heightData.get(),
mapData.terrain_alphaMapData.get()));

boost::shared_ptr<SkyPlane> skyPlane(
new SkyPlane(renderer,
mapData.skyPlane_position,
mapData.skyPlane_size,mapData.skyPlane_scale,
mapData.skyPlane_atmosphereRadius,mapData.skyPlane_tiling,
mapData.skyPlane_currentColor));

boost::shared_ptr<Water> water(
new Water(renderer,
mapData.water_position,
mapData.water_size,mapData.water_scale,
mapData.water_position.y,
terrain.get()));

boost::shared_ptr<Map> map(new Map(mapData.map_name,terrain,water,skyPlane));

world->setMap(map);
}

void MapDataChunkAll::save( std::ofstream& file ) const
{
//write the map name id and then the string to the file
unsigned int ID = MapDataChunkIDs::MAP_NAME_ID;
file.write((const char*)&ID,4);
STUtil::WriteStringToStream(file,map_name);

//write terrain data
writeIDAndData(file,MapDataChunkIDs::TERRAIN_SIZE_ID,(const char*)&terrain_size,sizeof(terrain_size));
writeIDAndData(file,MapDataChunkIDs::TERRAIN_SCALE_ID,(const char*)&terrain_scale,sizeof(terrain_scale));
writeIDAndData(file,MapDataChunkIDs::TERRAIN_PATCHSIZE_ID,(const char*)&terrain_patchSize,sizeof(terrain_patchSize));
writeIDAndData(file,MapDataChunkIDs::TERRAIN_HEIGHTDATA_ID,(const char*)terrain_heightData.get(),terrain_size*terrain_size*sizeof(float));
writeIDAndData(file,MapDataChunkIDs::TERRAIN_ALPHAMAPSIZE_ID,(const char*)&terrain_alphaMapSize,sizeof(terrain_alphaMapSize));
writeIDAndData(file,MapDataChunkIDs::TERRAIN_ALPHAMAPDATA_ID,(const char*)terrain_alphaMapData.get(),terrain_alphaMapSize*terrain_alphaMapSize*4);

//write skyplane data
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_ATMOSPHERE_ID,(const char*)&skyPlane_atmosphereRadius,sizeof(float));
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_CLOUDMOVE_ID,(const char*)&skyPlane_cloudMove,sizeof(skyPlane_cloudMove));
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_CURRENTCOLOR_ID,(const char*)&skyPlane_currentColor,sizeof(skyPlane_currentColor));
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_POSITION_ID,(const char*)&skyPlane_position,sizeof(skyPlane_position));
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_SCALE_ID,(const char*)&skyPlane_scale,sizeof(skyPlane_scale));
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_SIZE_ID,(const char*)&skyPlane_size,sizeof(skyPlane_size));
writeIDAndData(file,MapDataChunkIDs::SKYPLANE_TILING_ID,(const char*)&skyPlane_tiling,sizeof(skyPlane_tiling));

//write water data
writeIDAndData(file,MapDataChunkIDs::WATER_POSITION_ID,(const char*)&water_position,sizeof(water_position));
writeIDAndData(file,MapDataChunkIDs::WATER_SCALE_ID,(const char*)&water_scale,sizeof(water_scale));
writeIDAndData(file,MapDataChunkIDs::WATER_SIZE_ID,(const char*)&water_size,sizeof(water_size));

}

void MapDataChunkAll::load( std::ifstream& file )
{
unsigned int currentID = 0;

//loop by reading from the file
while(file.read((char*)&currentID,4))
{
switch(currentID)
{
//read the map name
case MapDataChunkIDs::MAP_NAME_ID:
map_name = STUtil::ReadStringFromStream(file);
break;
//read terrain
case MapDataChunkIDs::TERRAIN_HEIGHTDATA_ID:
// allocate for height data
terrain_heightData.reset(new float[terrain_size*terrain_size]);
file.read((char*)terrain_heightData.get(),terrain_size*terrain_size*sizeof(float));
break;
case MapDataChunkIDs::TERRAIN_ALPHAMAPDATA_ID:
// allocate for alphamap data
terrain_alphaMapData.reset(new unsigned char[terrain_alphaMapSize*terrain_alphaMapSize*4]);
file.read((char*)terrain_alphaMapData.get(),terrain_alphaMapSize*terrain_alphaMapSize*4);
break;
case MapDataChunkIDs::TERRAIN_SIZE_ID:
file.read((char*)&terrain_size,sizeof(terrain_size));
break;
case MapDataChunkIDs::TERRAIN_SCALE_ID:
file.read((char*)&terrain_scale,sizeof(terrain_scale));
break;
case MapDataChunkIDs::TERRAIN_PATCHSIZE_ID:
file.read((char*)&terrain_patchSize,sizeof(terrain_patchSize));
break;
case MapDataChunkIDs::TERRAIN_ALPHAMAPSIZE_ID:
file.read((char*)&terrain_alphaMapSize,sizeof(terrain_alphaMapSize));
break;
//read water
case MapDataChunkIDs::WATER_SIZE_ID:
file.read((char*)&water_size,sizeof(water_size));
break;
case MapDataChunkIDs::WATER_SCALE_ID:
file.read((char*)&water_scale,sizeof(water_scale));
break;
case MapDataChunkIDs::WATER_POSITION_ID:
file.read((char*)&water_position,sizeof(water_position));
break;
//read sky plane
case MapDataChunkIDs::SKYPLANE_SIZE_ID:
file.read((char*)&skyPlane_size,sizeof(skyPlane_size));
break;
case MapDataChunkIDs::SKYPLANE_SCALE_ID:
file.read((char*)&skyPlane_scale,sizeof(skyPlane_scale));
break;
case MapDataChunkIDs::SKYPLANE_POSITION_ID:
file.read((char*)&skyPlane_position,sizeof(skyPlane_position));
break;
case MapDataChunkIDs::SKYPLANE_ATMOSPHERE_ID:
file.read((char*)&skyPlane_atmosphereRadius,sizeof(skyPlane_atmosphereRadius));
break;
case MapDataChunkIDs::SKYPLANE_TILING_ID:
file.read((char*)&skyPlane_tiling,sizeof(skyPlane_tiling));
break;
case MapDataChunkIDs::SKYPLANE_CLOUDMOVE_ID:
file.read((char*)&skyPlane_cloudMove,sizeof(skyPlane_cloudMove));
break;
case MapDataChunkIDs::SKYPLANE_CURRENTCOLOR_ID:
file.read((char*)&skyPlane_currentColor,sizeof(skyPlane_currentColor));
break;
default:

STE::Log::write("Unrecognized ID = "+currentID,FUNCTIONINFO);
// this is processed only if an ID is read from the file
// but its not in our loading code.
// so its must be a data field which is removed in a new
// save file we need to handle it or skip it
handleOldData(currentID,file);
break;
}

}
}
void MapDataChunkAll::handleOldData(unsigned int ID,std::ifstream& file)
{
switch(ID)
{
//skip or handle old IDs that are not used anymore

default:
// If we reach here there are IDs in the MapDataChunkAllIDs
// that we don't check.This will fail the loading of old files
throw STE::RunTimeException("A field ID in an old save file is not handled!",FUNCTIONINFO);
break;

}

}
void MapDataChunkAll::writeIDAndData( std::ofstream& file,unsigned int ID,const char* data ,int size) const
{
file.write((const char*)&ID,4);
file.write(data,size);
}



This is working at the moment but I don't have different versions of save files yet.I will add saving/loading of entities on the the map and try to load maps withtout entities and see how it will work out.Also my entity classes are not done yet so they will change alot.
I only have a base entity class and a Tree class derived from it the buildings and objects are still using the base Entity class [looksaround].

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this