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.
typedef struct tagRESFILEHEADER
{
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);
VOID __stdcall (* lpUnload)(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.
lpStream->read(PCHAR(lpLump->lpData), lpLump->dwSize);
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;
}
//- resUnloadRawLump ----------------------------------------------------------
// Description: Unloads the data of a raw data lump.
// Parameters: lpLump - pointer to the lump node.
//-----------------------------------------------------------------------------
VOID __stdcall resUnloadRawLump(LPRESFILELUMP lpLump)
{
// 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);
VOID __stdcall (* lpUnload)(LPRESFILELUMP);
tagRESFILELUMPHANDLER *lpNextHandler;
} RESFILELUMPHANDLER, *LPRESFILELUMPHANDLER;
static BOOL m_bHandlerActive;
static LPRESFILELUMPHANDLER m_lpLumpHandlerList;
RESFILEHEADER m_resFileHeader; // Header structure for the resource file.
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 LoadLumpList(VOID);
BOOL SaveLumpList(VOID);
VOID UnloadLumpList(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:
// "r" - Read Mode
// "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
// consists of three functions, one each for loading, saving, and
// 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.
// lpLoad - pointer to the loading function.
// lpSave - pointer to the saving function.
// lpUnload - pointer to the unloading function.
// Returns: TRUE if successful, FALSE otherwise.
//----------------------------------------------------------------------------
static BOOL RegisterLumpHandler(
DWORD dwType,
BOOL __stdcall (* lpLoad)(fstream *, LPRESFILELUMP),
BOOL __stdcall (* lpSave)(fstream *, LPRESFILELUMP),
VOID __stdcall (* lpUnload)(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.
// dwType - Particular type of the lump (used for loading/saving)
// 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);
//- LoadLump -----------------------------------------------------------------
// Description: Loads in a lump from a resource file. Depending upon it's
// type, it will used the specially designed routine for loading
// 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.
//----------------------------------------------------------------------------
BOOL LoadLump(LPCSTR lpName, LPVOID *lplpData);
//- UnloadLump ---------------------------------------------------------------
// 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.
//----------------------------------------------------------------------------
BOOL UnloadLump(LPCSTR lpName);
//- Destructor ---------------------------------------------------------------
// Description: Deallocates any memory and closes the resource file if it
// is still open.
//----------------------------------------------------------------------------
~CResFile();
};
To open and load in a resource file's Lump Info Table, we first read in the header, seek to the position in the file where the Info Table begins. At this point, we load each entry in the table into a linked list. This is all done by Open() method of the CResFile class; additionally, this method sets up the header and the Lump Info Table if a new resource file is opened. Now, when someone wants to load in a data lump, the name of the lump would be passed to the LoadLump() method; this method searches through the Lump Info list until it finds a matching node. Once a match is found, it seeks to the position in the resource file where that particular data lump begins and gets ready to load it in. As was mentioned earlier, the lump handler list is searched at this point for a lump handler that matches the lump type. Once the lump has been loaded in, the LoadLump() method returns a pointer to the data or the lump's structure, depending upon the type of lump. Note that a lump is only loaded into memory once; moreover, if LoadLump() is messaged more than once using the same lump name, it will return the same pointer. If you want individual copies of a lump in memory, you must perform the "cloning" process on your own. Of course, you could always add functionality to the resource file manager to do this.
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.
|