• Create Account

Gamedev.net will be down tonight at approximately 9pm EST for site maintenance

Like
0Likes
Dislike

# Resource Files Explained

By Jesse Towner | Published Jan 17 2000 04:29 AM in Game Programming

file lump resource data files handler structure bool pointer
 If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

How This Document Is Organized
• Introduction
• Resource Files In Depth
• Design and Theory
• Implementation
• For the Future
Introduction

The time has come for you to venture into a new world of the game development universe - the elegant world of resource files. Perhaps you may be wondering what a resource file is, or you may have a vague idea of what they are and you're interested in using them. Perhaps you may even be fluent in the use of resource files. Regardless of which position you're in, we'll now go over what resource files are, and what they aren't. Let's begin by taking a non-technical approach to visualizing what they are; this way, we can have a much better overall understanding of the concept behind them. Suppose you have a large leather magic bag, which has the property of being essentially bottomless. You can take various items and put them in the bag, and you can these same items out. Although you don't necessarily have the option of taking a good gander at the contents of the bag, there is no problem in finding a particular item in order to take it out. This bag is also virtually weightless - you can easily hold it, carry it, and manage it. Now lets take this strange model of thought and apply it to the software-programming field, in our case, video game development. The magical leather bag becomes the resource file, and the items that we could put into the bag become the various pieces of data of which are used by the game – bitmaps, sounds, game-logic scripts, and so on. Additionally, the idea of being able to easily manage the bag still holds true, only in this case, we are managing the data in the resource file.

So basically, a resource file acts as an abstraction layer for managing multiple items of data; however, this is only one of many advantages that surface once you partake in the use of such files. Before we discuss the other advantages to using them, as well as going over a couple of the disadvantages, we will have a look at where resource files are already in use today.

Resource Files In Depth

Almost everywhere you look you will see some form of a resource file in use – perhaps even unknowingly. You may be wondering if resource files are in fact database files, and indeed, they are. Nevertheless, we still use the term "resource file" to discern between the two: a database usually keeps track of similar items of data, whereas a resource file has the ability of storing dissimilar items of data. Keeping this concept in mind, we can see that resource files are used by a great number of computer games as well as applications. Let's look at a couple specific examples of resource files in use. The "Zip File" archiving file format, in wide use today, is a great illustration of what a resource file is supposed to be. It can take multiple individual files and store them internally in a single file. Additionally, you have the option of compressing the files you add to a Zip file for optimum storage and transference. This is a good example because it shows you that you can take the data you want to add to a resource file and mutate it for the better without destroying it. Another example we'll look at is the WAD file format used by id Software in the Doom engine. The file format allowed the various items of game data to be stored in a single file – this included images, sprites, sounds, music, map data, and the likes. This will be the type of resource file format that we'll be most interested in, although we will implement our own original file format. Nonetheless, this will be that path that we take.

The question we must ask our selves now is "should we go through all of the trouble of implementing a resource file format?" Sure, you can also tackle security issues by creating your own file formats for the individual types of data like images, sounds, and on and on – plus, you can get around the complex directory structure problem. So, is there another reason for using resource files? The answer is undeniably yes. Due to the inefficiency of the file systems used by Windows and DOS (FAT16 and FAT32), a lot of disk space is lost. In the FAT16 file system, each file block takes up a minimum of 32KB; therefore, if you had a file that was 2KB in size, it in reality would take up 32KB. Of course, the FAT32 file system is much better, but it still isn't perfect. Anyway, when using resource files, all of the data is packed together, allowing for more efficient disk space usage.

Now, if those aren't good enough reasons for using resource files for storing data for your games, I don't know what is! Of course, there are a couple of disadvantages to using resource files. The first such disadvantage is that of requiring more time and effort to implement a good resource file manager. But once you have your implementation up and running, it's smooth riding from there on. The second disadvantage is that of requiring more memory to operate – you must use up a fair amount of RAM when working with resource files. Nonetheless, this is a trivial problem, especially considering that most PC's now ship with at least 64 megabytes of RAM. It is quite lucidly apparent that the advantages most certainly out-weigh the disadvantages – so what are we waiting for! Let's begin our adventure into the technical side of resource files.

Design and Theory

I'm sure you're quite worked up about jumping in and implementing a resource file management library, and heck, so am I! But first, we must go over some design issues. I will use the term "lump" to refer to a data item within a resource file; thus, a lump can be an image, or a game object, or any piece of data we may stick in a resource file. Also, when I talk of an interfacing API, I mean a front-end API that can be used to manage resource files. Now, in general, the structure of a resource file can be broken up into three sections: the header, the data lump information table, and the actual data lump section. The header section of a resource file contains general information about the file; for instance, the number of data lumps within the resource file, the location of the Lump Information Table within the file, and a means for identifying resource files from other file types. The Header section is critical because it provides the interfacing API with the information required in order loading the file in from disk. The Lump Information Table section of the resource file contains information about each data lump within the file. The information stored about each lump might entail it's position and size within the file, as well as it's name or identification medium. For each Data Lump in the resource file, there is one corresponding entry or node in the Lump Info Table; thus, the Info Table is a list of nodes where each node corresponds to and contains information about a particular Data Lump. The Data Lump Section is the section in the resource file where the data for each Data Lump is actually stored. The lumps are generally stored in a linear fashion such that the data for "Lump One" is to be located first, the data for "Lump Two" is to be found next, and so on. These data lumps may be compressed or encrypted depending upon the file format implementation desired by the programmer. However, I should mention the position of each of these sections within the file. For a few reasons that will later become apparent, we will position the Data Lump Info Table at the end of the file, having the actual data lump section in the middle. We do this because it is much more efficient to do so, if we didn't do this, we'd end up doing multiple passes during the save process. Anyway, as we now have the general structure of how we're going to set up our files, we'll discuss our interfacing API that we must design.

So, what do we need in a resource file management library? Well, the interface must be elegant and intuitive, whilst at the same time providing enough flexibility and scalability to deem it as reusable. Well, let's first discuss the user-friendly interface, then we'll talk about scalability. So, what sort of functionality are we going to need? We should provide routines for quick creation, modification, and clean up; hence, we'll need three routines for opening, saving, and closing resource files. We must also provide some routines for managing lumps; this will consist of routines for creating, deleting, modifying, loading, and unloading data lumps. Those are the basic routines we'll need; although, some extra routines for error checking and for displaying information about a resource file wouldn't hurt - but what about flexibility and scalability?

Indeed, this is something that we must take into consideration if we're going to use our resource file management library more than once. Basically, we should provide some means of being able to handle different data lump types within our manager. But how are we going to accomplish this? It's all quite simple really – we'll create a lump handler system. A lump handler will consist of three low-level routines, all for managing a particular data lump type. These routines would allow the interfacing API to load, to save, and to unload a particular data type. So, what we would do is create a lump handler for managing a bitmap lump type, a second handler for managing sound lumps, and so on. The interfacing API would then look at a lump, figure out which lump handler to use, and call the correct routines. In order to provide scalability, we should allow a programmer to create his or her very own custom lump handlers and allow the lump handler to be added to the lump-handling list on the fly. So, we'll need two more routines within our interfacing API: one to add a lump handler, another to remove a handler. Because we'll using OOP and C++ to implement our resource file management library, a lump handler will be an object that will be used by the resource file management class. Anyway, now that we have an outline of what we're going to need to do in order to get a good implementation happening, let's actually implement this baby!

Implementation

Let us now begin our design of the structures that will make up our resource file format. We'll start with the structure of the resource file's header.

// Structure defining the header of our resource files.
{
CHAR cSignature[4];      // 4-character identification value.
BYTE byVersionLo;        // Minor version.
BYTE byVersionHi;        // Major version.
WORD wFlags;         	// Special flags for the resource file.
DWORD dwNumLumps;        // Number of lumps in the resource file.
DWORD dwChecksum;        // Checksum (0FFFFFFFFh ^ file size).
DWORD dwLumpListOffset;  // Offset of the lump info list.
} RESFILEHEADER, *LPRESFILEHEADER;

As you can see, the header is indeed a critical part of a resource file - let's go over the various fields in this structure. The first field, "cSignature", is merely a file identification medium of which we use to identify resource files from other file types: it contains the non null-terminated string "RESF". The next two fields are the resource file's version numbers; we can use these values for keeping track of future revisions of the file format. The "wFlags" field is used to hold any special settings the resource file may have. The "dwNumLumps" holds the value indicating the number of data lumps that are currently within the file. Next, "dwChecksum" works as a simple and quick error checking method, which is used to make sure the size of the resource file was not illegally modified. Finally, "dwLumpListOffset" is the offset into the resource file where the data lump information table is stored.

Although the Lump Info Table is stored at the end of the file, we'll go over the structure that is used to create the table.

// Structure of each element in the lump info list. Holds information which
// can be used to retrieve the contents of a lump.
typedef struct tagRESFILELUMPINFO
{
DWORD dwSize;      // Size of the lump, in bytes.
DWORD dwOffset;    // Offset in the file of the lump.
DWORD dwType;      // The type of lump.
BYTE byNumChar;    // Length in characters of the lump's name.

// ... The lump's name string goes here, but it can be of variable length;
// therefore, we manually load it in to memory pointed to by 'lpName' in
// the RESFILELUMP structure.

} RESFILELUMPINFO, *LPRESFILELUMPINFO;

Basically, the Lump Info Table is an array of RESFILELUMPINFO structures. The "dwLumpListOffset" field of the RESFILEHEADER structure simply points to the first entry in this list. When a resource file is opened and loaded in, this entire table is loaded into memory as a linked list. This allows us to easily add new lumps to the resource file. However, when we load in resource file, we load the list into memory using another structure…

// Structure used to keep track of a lump in memory.
typedef struct tagRESFILELUMP
{
DWORD dwSize;      // Size of the lump, in bytes.
DWORD dwOffset;    // Offset in the file of the lump.
DWORD dwType;      // The type of lump.
BOOL bNoFree;      // Should we automatically free the data?
LPSTR lpName;      // Pointer to the name of the lump.
LPVOID lpData; 	// Pointer to the data of the lump.
LPVOID *lplpData;  // Pointer to the data address variable.
tagRESFILELUMP *lpNextLump;   // Pointer to the next lump in the list.
} RESFILELUMP, *LPRESFILELUMP;

Basically, this is the same as RESFILELUMPINFO, although this structure has a few more fields for managing lumps in memory. The "lpName" is the equivalent of a file name, only in this case, it happens to be a lump name. I should make a point of how the "lpData" field can be used. It doesn't necessarily have to point to a raw data buffer, it can also point to a structure. Then, it's merely a process of casting to the structure pointer type. This is useful when writing lump handlers that directly load lumps into structures; for example, loading a bitmap lump into a DirectDraw surface. Notice that the "dwType" field is evident in both of the lump structures. This is used as for identification when matching a particular lump with the corresponding lump handler. The management class just searches through the lump handler list, until it finds a match. Let's have a look at the structure used for keeping track of a lump handler.

typedef struct tagRESFILELUMPHANDLER
{
DWORD dwType;
BOOL __stdcall (* lpLoad)(fstream *, LPRESFILELUMP);
BOOL __stdcall (* lpSave)(fstream *, LPRESFILELUMP);
tagRESFILELUMPHANDLER *lpNextHandler;
} RESFILELUMPHANDLER, *LPRESFILELUMPHANDLER;

This structure also keeps a "dwType" field handy so that we can match it to a lump. Note the function pointers – they point to the functions that load, save, and unload a lump of that type. The loading and saving functions take a file stream pointer, along with a pointer to the lump's info. Because the management class takes care of positioning the file pointer to the beginning of the lump, writing these routines is rather trivial. In my implementation, I defined a "Raw Data" lump type that becomes the default lump type when one can't be determined. Here are the lump handling functions for the "Raw Data" lump type.

//- resLoadRawLump ------------------------------------------------------------
// Description: Loads in the data for a raw data lump.
// Parameters:  lpStream - pointer to the file stream object.
//              lpLump   - pointer to the lump node.
// Returns: 	TRUE if successful, FALSE otherwise.
//-----------------------------------------------------------------------------
BOOL __stdcall resLoadRawLump(fstream *lpStream, LPRESFILELUMP lpLump)
{
// First, allocate enough memory to the load the data into.
if ((lpLump->lpData = LPVOID(new CHAR[lpLump->dwSize])) == NULL)
return FALSE;

// Next, load in the data.
if (lpStream->fail())
{
delete [] PCHAR(lpLump->lpData);
lpLump->lpData = NULL;
return FALSE;
}

// It worked, so return true.
return TRUE;
}

//- resSaveRawLump ------------------------------------------------------------
// Description: Saves the data of a raw data lump to a file.
// Parameters:  lpStream - pointer to the file stream object.
//              lpLump   - pointer to the lump node.
// Returns: 	TRUE if successful, FALSE otherwise.
//-----------------------------------------------------------------------------
BOOL __stdcall resSaveRawLump(fstream *lpStream, LPRESFILELUMP lpLump)
{
// Write the data to the file.
lpStream->write(PCHAR(lpLump->lpData), lpLump->dwSize);
if (lpStream->fail())
return FALSE;

// It worked, so return true!
return TRUE;
}

// Description: Unloads the data of a raw data lump.
// Parameters:  lpLump   - pointer to the lump node.
//-----------------------------------------------------------------------------
{
// Delete the memory.
delete [] PCHAR(lpLump->lpData);
lpLump->lpData = NULL;
}

It's now time to take all of the structures and put them to good use; it's time to have a look at the resource file management class which happens to be our interfacing API.

// The actual Resource File Management class definition. The objects of the
// CResFile class can be used to create, load, or save resource files.
class CResFile
{
protected:
// Internal structure used to keep track of a lump handler.
typedef struct tagRESFILELUMPHANDLER
{
DWORD dwType;
BOOL __stdcall (* lpLoad)(fstream *, LPRESFILELUMP);
BOOL __stdcall (* lpSave)(fstream *, LPRESFILELUMP);
tagRESFILELUMPHANDLER *lpNextHandler;
} RESFILELUMPHANDLER, *LPRESFILELUMPHANDLER;

static BOOL m_bHandlerActive;
static LPRESFILELUMPHANDLER m_lpLumpHandlerList;

LPRESFILELUMP m_lpLumpList;      // Pointer to the root of the lump linked list.

CHAR m_cFileName[MAX_PATH];      // Local storage of the resource file's name.
CHAR m_cFileMode[4]; 			// Current file access mode.
fstream m_fStream;   			// File stream object.
fstream m_fSaveStream;   		// Temporary file stream object for save operations.

// Private internal methods.
LONG GetFileSize(VOID);
LPRESFILELUMPHANDLER GetLumpHandler(DWORD);
BOOL SaveLumpList(VOID);
BOOL SaveLumps(VOID);
static VOID Shutdown(VOID);

public:

//- Constructor --------------------------------------------------------------
// Description: Default constructor of this class - clears out data structures.
//----------------------------------------------------------------------------
CResFile();

//- Constructor --------------------------------------------------------------
// Description: See the CResFile::Open() method description.
//----------------------------------------------------------------------------
CResFile(LPCSTR lpFileName, LPCSTR lpFileMode)
{ this->Open(lpFileName, lpFileMode); }

//- Open ---------------------------------------------------------------------
// Description: Opens a resource file either for reading from, writing to, or
//              modification. Read mode opens up a resource file for read only
//              operations, and in this mode, the resource file can not be
//              modified. Write mode creates a new resource file (or overwrites
//              any existing file with the same name) and allows the programmer
//              to add lumps to the file. Modification mode opens up an existing
//              file (or creates one if one doesn't exist) and allows the
//              programmer to add more lumps to the file or to load in the lumps.
// Parameters:  lpFileName - name of the resource file (may include path) to open.
// 			lpFileMode - string containing the access mode, can be of the
//       					following contents:
// 		"w"  - Write Mode
// 		"r+" - Modification Mode
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------
BOOL Open(LPCSTR lpFileName, LPCSTR lpFileMode);

//- Save ---------------------------------------------------------------------
// Description: Saves the contents of a resource file in memory to disk. If
//              for some reason you want to save the file with an alternate
//              file name (i.e. "save as" operations), then pass the new file name
//              string pointer in the "lpAltFileName" parameter. Otherwise,
//              you can just pass NULL for this parameter to keep the original
//              file name.
// Parameters:  lpAltFileName - alternate file name.
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------
BOOL Save(LPCSTR lpAltFileName);

//- Close --------------------------------------------------------------------
// Description: Closes the resource file if one is currently open.
//----------------------------------------------------------------------------
VOID Close(VOID);

//- RegisterLumpHandler ------------------------------------------------------
// Description: Registers a lump handler with the CResFile class. A lump handler
//              unloading a lump of a particular type. You must also pass the
//              value which will used to identify whether lumps should be
//              handled by the lump handler or not - each value must be unique.
// Parameters:  dwType   - the type of lumps this handler will handle.
//              lpSave   - pointer to the saving function.
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------
static BOOL RegisterLumpHandler(
DWORD dwType,
BOOL __stdcall (* lpLoad)(fstream *, LPRESFILELUMP),
BOOL __stdcall (* lpSave)(fstream *, LPRESFILELUMP),
);

//- RemoveLumpHandler --------------------------------------------------------
// Description: Removes a lump handler previously added by a message to the
//              CResFile::RegisterLumpHandler() method.
// Parameters:  dwType - the lump type of the handler to remove.
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------
static BOOL RemoveLumpHandler(DWORD dwType);

//- LumpExists ---------------------------------------------------------------
// Description: Checks to see if a lump, with a particular name, exists.
// Parameters:  lpName - string of the lump to check for existence.
// Returns: 	TRUE if it exists, FALSE if it doesn't.
//----------------------------------------------------------------------------
BOOL LumpExists(LPCSTR lpName);

//- CreateLump ---------------------------------------------------------------
// Description: Creates a new lump and adds it to the active resource file.
//              Note that it will not be saved with the file unless the
//              CResFile::Save() method is messaged.
// Parameters:  lpName - Name of the lump.
//              lpData - Pointer to the data or data structure that will be
//   					stored in the lump.
//              dwSize - Size of the data in bytes (used only for RAW data lumps).
//              bFree  - Set this to TRUE if you want the data (pointed to by
//   					lpData) to be de-allocated when the file is closed.
// Returns: 	TRUE is successful, FALSE otherwise.
//----------------------------------------------------------------------------
BOOL CreateLump(
LPCSTR lpName,
DWORD dwType,
LPVOID lpData,
DWORD dwSize,
BOOL bFree=FALSE
);

//- DeleteLump ---------------------------------------------------------------
// Description: Removes or deletes a particular lump, designated by 'lpName',
//              from the active resource file.
// Parameters:  lpName - name of the lump to remove from the file.
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------
BOOL DeleteLump(LPCSTR lpName);

// Description: Loads in a lump from a resource file. Depending upon it's
//              it in. However, if such a routine doesn't exist, it will
//              default the lump as raw data.
// Parameters:  lpName   - name of the lump to load in.
//              lplpData - pointer to the location where the address of the data
//     					or data structure will be stored.
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------

// Description: Unloads a lump from memory that was previously loaded in from
//              a resource file using the CResFile::LoadLump() method.
// Parameters:  lpName - name of the lump to unload from memory.
// Returns: 	TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------

//- Destructor ---------------------------------------------------------------
// Description: Deallocates any memory and closes the resource file if it
//              is still open.
//----------------------------------------------------------------------------
~CResFile();
};

The process of saving a resource file is basically the same – we write out the header, then the data lumps, and finally the lump info table. When we first write out the header, we don't know of the position that the lump info table within the file; thus, we will have to seek to the beginning of the file and update the header section once all of the data has been saved. As the actual data lumps are being saved, we update the internal lump info linked list with the positions of the data lumps. Finally, we write out the Lump Info Table and return to the beginning of the file to update the header. This job is done by the Save() method of the CResFile class; of course, the file had to be opened using a write or modification access mode. Again, the lump handler list is queried for the correct lump handler when saving the lumps to the file.

Creating a new lump is simple; all we have to do is add a new node to the Lump Info Linked List and set a couple of flags in the node's structure indicating that we just created it. Deleting a lump is merely a process of removing it from the list. These functions are preformed by the CreateLump() and DeleteLump() methods of the CResFile class. If the lump type is not recognized, then the lump handler chosen will be the raw data lump handler.

Adding a custom lump handler is simpler than ever. Simply pass pointers of your lump handling functions to the RegisterLumpHandler() method along with the lump type for which the lump handler will process. You can also remove a lump handler using the RemoveLumpHandler() method.

I recommend that you have a good look at the source code at this point as it is well commented and it should give a good understanding of logic behind a resource file management library.

For the Future

Once you have your resource file management library up and running, you'll most likely want to make an editor so that you can easily create and manage various resource files. There are a couple of approaches you can for doing this. Probably the easiest way of doing this is creating a console application that accepts parameters on the command-line. You may also want to make an editor with a GUI that is similar to Windows Explorer. You could also incorporate a resource file editor into your game editor so that you have one application that acts as your tool set when creating the various resources. You will also probably want to create your own custom lump handling routines.

Still, is there anything else we can do to improve our resource file implementation? Of course there is, as the possibilities are limitless; however, I'll go over a few of them here. One thing you can do to organize the lumps in your resource files is by building an internal directory structure. This directory structure would emulate the one found in DOS, for instance. You could create numerous directories within the resource file and organize the data lumps into these directories. This leads to more efficient maintenance of resource files.

Another thing that would be interesting to do would be to create a data & file I/O streaming system that would be used by your resource file management library. You would create a base streaming class, and derive other streaming classes off of that. That way, you could have a compression streaming class, an encryption streaming class, an imbedded data streaming class, and so on. Then, you could add some settings to your resource file format that would allow you to compress and encrypt data as it's output to the file.

If you want to allow your resource file management library to be used to across a wide range of platforms you will have some extra work cut out for you. Of most concern would be the endian byte order used by the host platforms. You will have to take this into account by shifting the bytes around so that they will be compatible with the machine; this process would take place during the loading and saving of a resource file.

Another concept, which is interesting, is that of data caching. Since accessing a hard drive is much faster than reading off of a CD or DVD drive, a temporary cache file could be created on the hard drive. Then, the most frequently accessed data would be placed in the cache file for quick access. This would greatly speed up the loading sequences in your game once the cache file is created and operational.

Of course, there are many other things you could do with a resource file management library – it all depends on what you want to use the management library for. Anyway, I hope you've enjoyed this article, good luck with your coding endeavors, and code-on!