Ce tutoriel est largement inspiré d'un très bon article de Raymond Wilson. Je n'ai utiliser que l'article pour la fonction Create, le reste est de moi :)
Pour faire simple, nous allons regrouper tous nos fichiers ressources dans un seul fichier .DAT. L'article gère le cryptage des fichiers, mais pour le moment nous ne ferons que mettre les fichiers directement sans aucun cryptage et sans aucune compression.
Le fichier sera structuré en trois parties principales :
Pour cela, on va créer une structure pour les deux premières parties :
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 };
La structure sDATHeader contient les informations du fichier lui-même. Ici, pour cette première version, on ne retiendra qu'un mot clef qui nous permettra d'identifier qu'il s'agit bien d'un fichier au format que nous utilisons (UniqueID), mais aussi un numéro de version (version). Enfin, nous devons bien entendu connaitre le nombre de fichiers présents dans notre fichier DAT.
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 contient les informations d'un fichier stocké. On retiendra le nom, la taille et la position dans le fichier DAT de chacun de nos ifchiers stockés.
Bien entendu, comme sFileEntry nous renseigne sur un seul des fichiers stockés, notre fichier DAT devra connaitre tous les fichiers, et pour cela nous utiliserons un std::vector. Dans un souci de simplicité, nous ne gérerons pas une structure de répertoires (Ce n'est que la version 1 ! ;) ).
Notre objet cDAT peut donc maintenant être créé :
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); };
Les méthodes de notre objet nous permettent de créer et d'utiliser un fichier DAT :
Le constructeur et le destructeur servent simplement à initialiser le buffer et à le nettoyer proprement :
cDAT::cDAT (void) { m_buffer = NULL; } cDAT::~cDAT (void) { if (m_buffer!=NULL) delete (m_buffer); }
Pour le moment, utiliser la méthode Create est le seul moyen pour créer notre fichier DAT. Ce n'est pas très pratique, mais on a l'avantage de ne pas avoir à en créer un tout le temps. Nous conserverons donc ce système pour le moment. La méthode Create est un peu lourde :
bool cDAT::Create (std::vector<std::string> files, std::string destination) { //An 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); }
Bon, maintenant qu'on a un fichier créé et qui contient quelques ressources, on veut l'exploiter. Les méthodes qui suivent sont celles qui sont vraiment utiles dans une application !
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(); }
Cette fois, on ne va faire que lire les informations contenues dans le fichier DAT. Lire tous les fichiers pourrait être long, et n'est pas forcément utile, on réserve cela pour plus tard, lorsqu'on aura réellement besoin d'un contenu.
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); }
Maintenant, grâce à cette fonction, on peut lire le contenu complet d'un fichier et le stocker en mémoire. Désormais, le buffer alloué à la lecture sera desalloué à la prochaine lecture, ou bien il sera desalloué via le destructeur. Cela simplifie l'utilisation du buffer pour l'utilisateur.
La dernière fonction sert à connaitre la taille du fichier en mémoire. Cela parce que le pointeur retourné ne connait pas lui-même sa propre taille :
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à ! On va maintenant pouvoir utiliser une petite application pour vérifier tout ca :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; }
Et voilà ! On a maintenant un fichier DAT fonctionnel que l'on peut relire et qui nous permet d'empaqueter toutes nos sources. Pour ce qui concerne un fichier de configuration que l'utilisateur peut modifier, on préferera le laisser à l'extérieur sinon il faudrait reconstruire le fichier .DAT à chaque fois !