Datafile routines
Datafiles are created by the grabber utility (see grabber.txt for more
information), and have a `.dat' extension. They can contain bitmaps, palettes,
fonts, samples, MIDI music, FLI/FLC animations, and any other binary data that
you import. You could distribute your bitmaps and samples in a myriad of
separate files, but packing them in a few `.dat' binaries has a few
advantages:
-
On some platforms loading a single big datafile at once is faster than
loading individual resources one by one.
-
Instead of several loops for your resources, you can write a single line of
code with just a single point of failure to take care of.
-
You can potentially reduce the size of your data by enabling compression
on your datafiles. Less download time for your end users, less wait during
loading screens!
-
If you don't need to load the whole datafile at once, you can still enable
individual file compression. It is slightly worse than global compression,
but it is very fast with loading times because Allegro can easily seek
inside the datafile to find a specific object.
-
Even without encryption, most end users of your application won't be able
to look at or modify the resources for your game. A missing sound file or
a modified bitmap could potentially crash the game if you haven't
considered this in your loading code!
-
It looks much more professional and convenient to distribute levels! For
example, if you found a bug in a level of your game, just distribute your
new `level4.dat' and tell users to overwrite their old version.
Allegro allows you to load datafiles once and forget about them. But if you
have many levels it can be wise to load only the resources required for the
current level. You can accomplish the later by separating levels in different
datafiles, or using functions like load_datafile_object() to avoid loading
everything at once. You can even read directly from a specific datafile object
with the pack_fopen() function.
On some platforms you can attach datafiles to your binary, potentially
reducing your game distribution down to a single executable file. Try the
example exexedat on your platform to see if this is possible. However, this
is not recommended for big programs: a single update to your code or binary
data would force end users to download again a potentially big file, no matter
how small your change is. The same warning goes for the tools dat2s or dat2c,
which convert datafiles into assembler and C code respectively, prepared to be
included directly in your binary.
Remember that with Allegro truecolor images can only be loaded after you have
set a graphics mode. This is true for datafiles too. Load all your data after
you have set the graphics mode, otherwise the pixel format (RGB or BGR) will
not be known and the datafile may be converted wrongly. Oh, and read carefully
the warning of fixup_datafile() if you plan on switching resolutions during
runtime.
Note: even though Allegro datafiles provide encryption, you should consider
it weak, so don't plan on hiding there the plans for a Death Star or
something. Determinate knowledgeable users will be able to rip your resources
no matter how hard you try to hide them! Use the encryption only as a slight
deterrent towards unwanted tampering of your data. How to crack an encrypted
datafile is left as an exercise to the reader, though.
Using datafiles once they are loaded is quite simple: you access the elements
of the DATAFILE as a normal array. Read below the section "Using datafiles"
below for several examples on how to access their data.
Using datafiles
In order to access the contents of a datafile, you will need to know where
each object is located. The easiest way to do this is by integer index,
using an automatically generated header file. With the grabber, type a name
into the "Header:" field, and the object indexes will be written to this
file whenever the datafile is saved. With the dat utility, use the '-h'
option, eg. "dat filename.dat -h filename.h". The header will define C
preprocessor symbols for each object in the datafile, for example:
#define SOME_DATA 0 /* DATA */
#define SOME_MORE_DATA 1 /* DATA */
To prevent name conflicts, you can specify a prefix string for these
definitions by typing it into the "Prefix:" field in the grabber or using
the '-p' option to dat.
To load a datafile into memory, call the function:
DATAFILE *load_datafile(char *filename);
This will load the entire file, returning a pointer to it, or NULL on error.
When the data is no longer required, the entire thing can be destroyed by
calling:
void unload_datafile(DATAFILE *dat);
When you load a datafile, you will obtain a pointer to an array of DATAFILE
structures:
typedef struct DATAFILE
{
void *dat; - pointer to the actual data
int type; - object type ID
long size; - size of the data, in bytes
DATAFILE_PROPERTY *prop; - list of object properties
} DATAFILE;
The only really important piece of information here is the `dat' field, which
points to the contents of the object. What type of data this is will depend
on the type of object: for bitmaps it will be an Allegro BITMAP structure,
for RLE sprites an RLE_SPRITE, for fonts a FONT structure, etc. If you are
programming in C you can pass this pointer directly to the relevant Allegro
library functions, but if you are using C++ you will need to cast it to the
appropriate type to prevent the compiler giving a warning.
For example, if you have a datafile called `myfile.dat', which contains a
bitmap called COOL_PICTURE, and you have used it to produce a header called
`myfile.h', you could display the bitmap with the code:
#include "myfile.h"
void show_the_bitmap()
{
DATAFILE *dat;
BITMAP *bmp;
dat = load_datafile("myfile.dat");
if (!dat) {
/* report an error! */
return;
}
bmp = (BITMAP *)dat[COOL_PICTURE].dat;
blit(bmp, screen, 0, 0, 0, 0, bmp->w, bmp->h);
unload_datafile(dat);
}
If a datafile contains nested child datafiles, the header will prefix the
names of objects in the sub-files with the name of their parent datafile. It
will also define a count of the number of objects in the child file, which
may be useful if for example the child datafile contains several bitmaps
which form a 'run' animation, and you want your code to automatically
adjust to the number of frames in the datafile.
For example, the following datafile:
"FILE" - NESTED_FILE
|- "BMP" - A_BITMAP
|- "FONT" - A_FONT
"DATA" - SOME_DATA
"DATA" - SOME_MORE_DATA
Will produce the header:
#define NESTED_FILE 0 /* FILE */
#define NESTED_FILE_A_BITMAP 0 /* BMP */
#define NESTED_FILE_A_FONT 1 /* FONT */
#define NESTED_FILE_COUNT 2
#define SOME_DATA 1 /* DATA */
#define SOME_MORE_DATA 2 /* DATA */
The main datafile contains three objects (NESTED_FILE, SOME_DATA, and
SOME_MORE_DATA) with consecutive indexes, while the child datafile contains
the two objects A_BITMAP and A_FONT. To access these objects you need to
reference both the parent and child datafiles, eg:
DATAFILE *dat = load_datafile("whatever.dat");
DATAFILE *nested = (DATAFILE *)dat[NESTED_FILE].dat;
FONT *thefont = (FONT *)nested[NESTED_FILE_A_FONT].dat;
If you need to access object property strings from within your program, you
can use the function:
char *get_datafile_property(DATAFILE *dat, int type);
This will return a pointer to the property string if it can be found, and an
empty string (not null!) if it does not exist. One possible use of this
function is to locate objects by name, rather than using the indexes from a
header file. The datafile array is ended by an object of type DAT_END, so to
search the datafile dat for the object "my_object" you could use the code:
const int name_type = DAT_ID('N','A','M','E');
for (i=0; dat[i].type != DAT_END; i++) {
if (stricmp(get_datafile_property(dat+i, name_type),
"my_object") == 0) {
/* found the object at index i */
}
}
/* not found... */
If you prefer to access objects by name rather than index number, you can
use the function:
DATAFILE *find_datafile_object(DATAFILE *dat, char *objectname);
This will search an already loaded datafile for an object with the specified
name, returning a pointer to it, or NULL if the object cannot be found. It
understands '/' and '#' separators for nested datafile paths.
It is also possible to selectively load individual objects from a datafile,
with the function:
DATAFILE *load_datafile_object(char *filename, char *objectname);
This searches the datafile for an object with the specified name, so
obviously it won't work if you strip the name properties out of the file.
Because this function needs to seek through the data, it will be extremely
slow if you have saved the file with global compression. If you are planning
to load objects individually, you should save the file uncompressed or with
individual compression per-object. Because the returned datafile points to a
single object rather than an array of objects, you should access it with the
syntax datafile->dat, rather than datafile[index].dat, and when you are done
you should free the object with the function:
void unload_datafile_object(DATAFILE *dat);
Example:
music_object = load_datafile_object("datafile.dat", "MUSIC");
play_midi(music_object->dat);
...
unload_datafile_object(music_object);
Alternatively, the packfile functions can open and read directly from the
contents of a datafile object. You do this by calling pack_fopen() with a
fake filename in the form "filename.dat#object_name". The contents of the
object can then be read in an identical way to a normal disk file, so any of
the file access functions in Allegro (eg. load_pcx() and set_config_file())
can be used to read from datafile objects. Note that you can't write to
datafiles in this way: the fake file is read only. Also, you should save the
file uncompressed or with per-object compression if you are planning on
using this feature. Finally, be aware that the special Allegro object types
aren't the same format as the files you import the data from, so if for
example you want to use load_pcx to read an image from a datafile, you
should import it as a binary data chunk rather than as a BITMAP object.
If you have appended a datafile to the end of your executable with the
exedat utility, use load_datafile("#") to read the entire thing into memory,
load_datafile_object("#", "object_name") to load a specific object, and
pack_fopen("#object_name", F_READ) to read one of the objects directly with
your own code. Note that unless you use the previous functions to load the
appended data, the OS will not load it into memory just because you are
running the program, so you shouldn't have problems attaching datafiles to
your binary larger than the available system memory.
By default, all graphic objects loaded from a datafile will be converted
into the current color depth. This conversion may be both lossy and very
slow, particularly when reducing from truecolor to 256 color formats, so you
may wish to disable it by calling set_color_conversion(COLORCONV_NONE) or
set_color_conversion(COLORCONV_PARTIAL) before your call to load_datafile().
Custom datafile objects
Some of the objects in a datafile, for example palettes and FLI animations,
are simply treated as blocks of binary data, but others are loaded into
special formats such as bitmap structures or compiled sprites. It is
possible to extend the datafile system to support your own custom object
types, eg. map objects for a tile based engine, or level data for a platform
game. Obviously the grabber has no way of understanding this data, but it
will allow you to import binary data from external files, so you can grab
information produced by your own utilities. If you are happy with the data
being loaded as a simple binary block, that is all you need to do, but if
you need to load it into a specific structure, read on...
Your custom objects must be given a unique type ID, which is formed from
four ASCII characters (by convention all uppercase A-Z). If you don't use
all four characters, the string should be padded with spaces (ASCII 32). You
should use this ID when creating the objects in the grabber (select
New/Other and type in the ID string), and in your code you should define an
identifier for the type, eg:
#define DAT_MAPDATA DAT_ID('M','A','P','D')
You then need to write functions for loading and destroying objects of this
type, in the form:
void *load_mapdata(PACKFILE *f, long size)
{
/* Allegro will call this function whenever an object of your custom
* type needs to be loaded from a datafile. It will be passed a
* pointer to the file from which the data is to be read, and the size
* of the object in bytes. It should return a pointer to the loaded
* data, which will be stored in the dat field of the datafile object
* structure, or NULL if an error occurs. The file will have been
* opened as a sub-chunk of the main datafile, so it is safe to read
* past the end of the object (if you attempt this, Allegro will
* return EOF), and it is also safe to return before reading all the
* data in the chunk (if you do this, Allegro will skip any unused
* bytes before starting to read the next object). You should _not_
* close the file when you are done: this will be handled by the
* calling function. To clarify how all this works, here's an example
* implementation of a null-terminated string object:
*/
#define MAX_LEN 256
char buf[MAX_LEN];
char *p;
int i, c;
for (i=0; i<;MAX_LEN-1; i++) {
if ((c = pack_getc(f)) == EOF)
break;
buf[i] = c;
}
buf[i] = 0;
p = malloc(i+1);
strcpy(p, buf);
return p;
}
void destroy_mapdata(void *data)
{
/* Allegro will call this function whenever an object of your custom
* type needs to be destroyed. It will be passed a pointer to the
* object (as returned by the load function), and should free whatever
* memory the object is using. For example, the simple string object
* returned by the above loader could be destroyed with the code:
*/
if (data)
free(data);
}
Finally, before you load your datafile you must tell Allegro about the
custom format, by calling:
register_datafile_object(DAT_MAPDATA, load_mapdata, destroy_mapdata);
It is also possible to integrate support for custom object types directly
into the grabber and dat utilities, by copying some special files into the
tools/plugins directory. This can be used to add whole new object types and
menu commands, or to provide additional import/export routines for the
existing formats. See `tools/plugins/plugins.txt' for an overview of how to
write your own grabber plugins.