Intégrer ses ressources dans un seul fichier DAT

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 :

  • L'entête
  • La table des fichiers
  • Les données (les fichiers)

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 de l'objet
  • Create : utilise un vecteur de fichiers et créer le fichier principal en stockant tout le contenu du vecteur dedans
  • Read : lit un fichier .DAT et stocke les informations fichiers de celui-ci
  • GetFile : renvoie un pointeur contenant le fichier en mémoire.
  • GetFileSize : renvoie la taille d'un fichier stocké

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 !

 
fr/tutoriels/formatdat.txt · Last modified: 2009/03/29 22:57 by daazku
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki