This tuto is widely inspired from this very good article from Raymond Wilson. I used it only for the Create method, other code is from me :)
The idea is simple : we are going to pack all our files into one single .DAT file. The article is talking about encrypting the files added, but at the moment, we will only pack them qithout any encryption nor compression.
The structure of the .DAT file will be in three parts :
In order to do that, we have to create a structure for the Header and the Table :
struct sDATHeader { char uniqueID[5]; /// Unique ID used to know if this file is a DAT File from this class char version[3]; /// Version of the DAT file format unsigned int nb_files; /// Number of files in the DAT file };
This structure contains informtions about the file itself. Here, we are simply going to use a keyword to be sure that the file is from our application (the uniqueID), bu we are also using a version number (version). At last, we have to know how many files we are going to pack in this .DAT file.
struct sFileEntry { char name[300]; /// Name of the data file long size; /// Size of the data file long offset; /// Offset, in the DAT file where the file is };
sFileEntry contains all the informations about each file packed. For this, we use the name, the size, and the position of the file inside the .DAT file.
Of course, as sFileEntry is a structure for only one file, we have to use a std::vector in order to list all the files. For this tuto, we won't manage the directory stuff (Eh this is only a tuto !).
Now, we can create our cDAT object :
class cDAT { private : std::string m_datfile; /// name of the DAT file sDATHeader m_header; /// file header std::vector<sFileEntry> m_entries; /// vector of files entries char* m_buffer; /// Buffer pointing on a file in memory public : cDAT (void); ~cDAT (void); bool Create (std::vector<std::string> files, std::string destination); void Read (std::string source); char* GetFile (std::string filename); long int GetFileSize (std::string filename); };
Those functions permit us to use a .DAT file :
Consructor and destructor are here only to initialize the buffer and to clean it properly :
cDAT::cDAT (void) { m_buffer = NULL; } cDAT::~cDAT (void) { if (m_buffer!=NULL) delete (m_buffer); }
At the moment, you must use the Create function to create the .DAT file. This is not very useful, but you don't have to reate one every time once your files are packed. For the tuto, we are not modifying this method.
The Create function is maybe a lot more complex :
bool cDAT::Create (std::vector<std::string> files, std::string destination) { //A file entry in order to push it in the object's std::vector sFileEntry entry; //An input file stream to read each file included std::ifstream file; //An output file stream to write our DAT file std::ofstream datfile; //The buffer used to read/write the DAT file char buffer[1]; //DATHeader //We start by filling it with 0 memset (&m_header, 0, sizeof(m_header)); //Then we copy the ID memcpy (m_header.uniqueID, "JGDAT", 5); //Then the version memcpy (m_header.version, "0.1", 3); //Then the number of files to include m_header.nb_files = files.size(); //Next, we open each file in orderto create the File Entries Table for (unsigned int i = 0; i<files.size(); i++) { file.open (files[i].c_str(), std::ifstream::in | std::ifstream::binary); if (file.is_open()) { //Filling the FileEntry with 0 memset (&entry, 0, sizeof(sFileEntry) ); //We keep the file name memcpy (entry.name, files[i].c_str(), strlen ( files[i].c_str() ) ); //We calculate its size file.seekg (0, std::ios::end); entry.size = file.tellg(); //Since we don't know exactly its final position in the DAT file, let's use 0 entry.offset = 0; //We finished with this file file.close(); //Finally, we add this File Entry in our std::vector m_entries.push_back(entry); } else { //Simple error track std::cout<<"File "<<files[i]<<" raise an error."<<std::endl; return (false); } } //Now, we know everything about our files, we can update offsets long actual_offset = 0; actual_offset += sizeof(sDATHeader); actual_offset += m_header.nb_files * sizeof(sFileEntry); for (unsigned int i=0;i<m_entries.size();i++) { m_entries[i].offset = actual_offset; actual_offset += m_entries[i].size; } //And finally, we are writing the DAT file datfile.open (destination.c_str(), std::ofstream::out | std::ofstream::binary); //First, we write the header datfile.write ((char*)&m_header, sizeof(sDATHeader) ); //Then, the File Entries Table for (unsigned int i=0;i<m_entries.size();i++) { datfile.write ((char*)&m_entries[i], sizeof(sFileEntry) ); } //Finally, we write each file for (unsigned int i = 0; i<m_entries.size(); i++) { file.open (m_entries[i].name, std::ifstream::in | std::ifstream::binary); if (file.is_open()) { file.seekg (0, std::ios::beg); while (file.read (buffer, 1)) { datfile.write (buffer, 1); } file.close(); } file.clear(); } //And it's finished datfile.close(); return (true); }
Good, now we have a .DAT file, we can try to read it. Next functions will be the ones used in your final app.
void cDAT::Read (std::string source) { //The input file stream from which we want informations std::ifstream datfile; //A file entry in order to push it in the object's std::vector sFileEntry entry; //Filling the header with 0 memset (&m_header, 0, sizeof(m_header)); //We open the DAT file to read it datfile.open (source.c_str(), std::ifstream::in | std::ifstream::binary); if (datfile.is_open()) { //Getting to the Header position datfile.seekg (0, std::ios::beg); //Reading the DAT Header datfile.read ((char*)&m_header, sizeof(sDATHeader)); //Next we are reading each file entry for (unsigned int i=0;i<m_header.nb_files;i++) { //Reading a File Entry datfile.read ((char*)&entry, sizeof(sFileEntry)); //Pushing it in our std::vector m_entries.push_back(entry); } //Since all seems ok, we keep the DAT file name m_datfile = source; } //Closing the DAT file datfile.close(); }
This time, we only read informations from the .DAT file. We don't want to read every single files in it, it will be done later only if needed.
char* cDAT::GetFile (std::string filename) { //The input file stream from which we want information std::ifstream datfile; //Cleaning properly an ancient file loaded if (m_buffer != NULL) { delete (m_buffer); m_buffer = NULL; } //First, we have to find the file needed for (unsigned int i=0; i<m_header.nb_files;i++) { //If we found it if (m_entries[i].name == filename) { //We are allocating memory to the buffer m_buffer = new char[(m_entries[i].size)]; //Simple error catch if (m_buffer==NULL) return (NULL); //Opening the DAT file ot read the file datas needed datfile.open (m_datfile.c_str(), std::ifstream::in | std::ifstream::binary); if (datfile.is_open()) { //Going to the right position datfile.seekg (m_entries[i].offset, std::ios::beg); //Reading datfile.read (m_buffer, m_entries[i].size); //We can close the DAT file datfile.close(); //Returning the buffer return (m_buffer); } } } //Finally, there is no such file in our DAT file return (NULL); }
With this function, you can read a complete file and keep it in memory. For simplicity, the buffer will be cleaned at the next reading, or at the destruction of the object. It's simplier for the end developer.
The last function is used to know the size of the file in memory. This is because the pointer is not sufficient to use the file.
long int cDAT::GetFileSize (std::string filename) { //First, we have to find the file needed for (unsigned int i=0; i<m_header.nb_files;i++) { //If we found it if (m_entries[i].name == filename) { //Returning the size of the file found return (m_entries[i].size); } } return (0); }
Et voilĂ ! We can, now, use it in a little app to verify everything is ok :o)
int main() { //Variables for creation cDAT write_test; std::vector<std::string> toto; //Let's creating a DAT file toto.push_back("test7b.bmp"); toto.push_back("fond.wav"); toto.push_back("test.xml"); if (!write_test.Create(toto, "test.dat")) { return EXIT_FAILURE; } //Ok, we got a magnific DAT file named "test.dat" //Now, let's try to read it cDAT read_test; char* buffer; //Getting files informations read_test.Read("test.dat"); //Now let's try to get a simple xml file buffer = read_test.GetFile("test.xml"); if (buffer==NULL) { //Simple error catch std::cout<<"Reading error"<<std::endl; } else { //Ok, read, let's print it on screen std::cout<<buffer<<std::endl; } //Now let's try to get a simple xml file which doesn't not exists buffer = read_test.GetFile("testa.xml"); if (buffer==NULL) { //Simple error catch std::cout<<"Reading error"<<std::endl; } else { //Ok, read, let's print it on screen std::cout<<buffer<<std::endl; } //Wouh ! Let's go test it in SFML now ! //SFML variables sf::RenderWindow App(sf::VideoMode(800, 600, 32), "SFML Demo 4"); sf::Event Event; sf::Image i1; sf::Sprite s1; sf::Image i2; sf::Sprite s2; sf::Music m; bool Running = true; //Let's read the image file buffer = read_test.GetFile("test7b.bmp"); if (buffer==NULL) { //simple error catch std::cout<<"Read error"<<std::endl; return EXIT_FAILURE; } //Ok, now we can load the image by using memory instead of a file i1.LoadFromMemory(buffer, read_test.GetFileSize("test7b.bmp")); //SFML instructions s1.SetImage(i1); s1.SetSubRect(sf::IntRect(0, 0, 100, 100)); //We try the LoadFromFile function to ensure images are the same i2.LoadFromFile("test7b.bmp"); s2.SetImage(i2); s2.SetSubRect(sf::IntRect(0, 0, 100, 100)); s2.SetPosition(0,110); //Now, let's get the music file buffer = read_test.GetFile("fond.wav"); if (buffer==NULL) { //simple error catch std::cout<<"Read error"<<std::endl; return EXIT_FAILURE; } //Ok, now we can load the music by using memory instead of a file m.OpenFromMemory(buffer, read_test.GetFileSize("fond.wav")); //WARNING, deleting the buffer causes app to crash because //the sf::Music keep a link on our memory //You must make a copy of memory or not load any more files //SFML instructions m.SetLoop(true); m.Play(); //Main loop while (Running) { //Manage Events while (App.GetEvent(Event)) { // Window closed if (Event.Type == sf::Event::Closed) { Running = false; } //Key pressed if (Event.Type == sf::Event::KeyPressed) { switch (Event.Key.Code) { case sf::Key::Escape : Running = false; break; //Pause/Unpause music case sf::Key::Space : if (m.GetStatus() == sf::Music::Paused) { m.Play(); } else { m.Pause(); } break; default : break; } } } //Drawing sprites using Image from memory and image from a file App.Draw(s1); App.Draw(s2); App.Display(); } return EXIT_SUCCESS; }
We have a fully functional .DAT file and we can use it easily. For the config file, it's better to keep it outside in order to let the user modify it.