Stock all your resources in a single DAT file

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 :

  • Header
  • Files table
  • Datas (the files)

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 :

  • The constructor and the destructor
  • Create : Using a vector of files, create the main .DAT file and pack every files in it
  • Read : Read a .DAT file and grab all the files informations
  • GetFile : Return a packed file pointer
  • GetFileSize : Return the size of a packed 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.

 
en/tutorials/formatdat.txt · Last modified: 2011/03/14 19:19 by mecablaze
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki