Editors note: This tutorial has been updated in relation
with the registry. Some of the functions are renamed, and some are
slightly different. Therefore it's highly recommended to use the "Updated File Access from QuakeC" tutorial.
With all due respect to Quake Engine Resources,
their QuakeC file tutorial certainly isn't up to par with the rest of
their code. While they create awesome graphics code, not ever being QC
Coders they over looked some of the quirks of QuakeC, and some of their
functions simply do not work. At the request of many in the QuakeC
community, I have created the following tutorial which adds "better"
QuakeC file support to the engine. It's a bit more limited than QER's
version, but it's also much simpler to use. (It's output is also
human-readable)
In addition, included are functions for string manipulation &
handling in QuakeC. These are needed because all file I/O in this
tutorial is done with strings. Most of these functions can also be used
in a wide variety of other circumstances. Anyway, lets get started.
First the engine code, then a little explanation of how the functions
work in QC. Open up pr_cmds.c in your IDE of choice, and find PF_Fixme
near the bottom of the file. Above it, copy and paste this gigantic
function set:
// 01-23-2000 FrikaC QuakeC string manipulation Begin
void PF_zone (void)
{
char *m, *p;
m = G_STRING(OFS_PARM0);
p = Z_Malloc(Q_strlen(m) + 1);
Q_strcpy(p, m);
G_INT(OFS_RETURN) = p - pr_strings;
}
void PF_unzone (void)
{
Z_Free(G_STRING(OFS_PARM0));
G_INT(OFS_PARM0) = OFS_NULL; // empty the def
};
void PF_strlen (void)
{
char *p = G_STRING(OFS_PARM0);
G_FLOAT(OFS_RETURN) = strlen(p);
}
char pr_strcat_buf [128]; // need this becuase pr_string_temp sucks
void PF_strcat (void)
{
char *s1, *s2;
memset(pr_strcat_buf, 0, 127);
s1 = G_STRING(OFS_PARM0);
s2 = PF_VarString(1);
strcpy(pr_strcat_buf, s1);
strcat(pr_strcat_buf, s2);
G_INT(OFS_RETURN) = pr_strcat_buf - pr_strings;
}
void PF_substring (void)
{
int ltwo, start;
char *p, *d;
d = pr_string_temp;
p = G_STRING(OFS_PARM0);
start = (int)G_FLOAT(OFS_PARM1); // for some reason, Quake doesn't like G_INT
ltwo = (int)G_FLOAT(OFS_PARM2);
if (start > strlen(p))
start = strlen(p) - 1;
// cap values
if (start < 0)
start = 0;
if (ltwo < 0)
ltwo = 0;
p += start;
Q_strncpy(d, p, ltwo);
G_INT(OFS_RETURN) = pr_string_temp - pr_strings;
}
// thanks zoid
void PF_stof (void)
{
char *s;
s = G_STRING(OFS_PARM0);
G_FLOAT(OFS_RETURN) = atof(s);
}
void PF_stov (void)
{
char *v;
int i;
vec3_t d;
v = G_STRING(OFS_PARM0);
for (i=0; i<3; i++)
{
while(v && (v[0] == ' ' || v[0] == '\'')) //skip unneeded data
v++;
d[i] = atof(v);
while (v && v[0] != ' ') // skip to next space
v++;
}
VectorCopy (d, G_VECTOR(OFS_RETURN));
}
// 01-23-2000 FrikaC QuakeC string manipulation End
// 01-23-2000 FrikaC QuakeC file system Begin
void PF_open (void)
{
char *p = G_STRING(OFS_PARM0);
char *ftemp;
int fmode = G_FLOAT(OFS_PARM1);
int h = 0, fsize = 0;
switch (fmode)
{
case 0: // read
Sys_FileOpenRead (va("%s/%s",com_gamedir, p), &h);
G_FLOAT(OFS_RETURN) = (float) h;
return;
case 1: // append -- this is nasty
// copy whole file into the zone
fsize = Sys_FileOpenRead(va("%s/%s",com_gamedir, p), &h);
if (h == -1)
{
h = Sys_FileOpenWrite(va("%s/%s",com_gamedir, p));
G_FLOAT(OFS_RETURN) = (float) h;
return;
}
ftemp = Z_Malloc(fsize + 1);
Sys_FileRead(h, ftemp, fsize);
Sys_FileClose(h);
// spit it back out
h = Sys_FileOpenWrite(va("%s/%s",com_gamedir, p));
Sys_FileWrite(h, ftemp, fsize);
Z_Free(ftemp); // free it from memory
G_FLOAT(OFS_RETURN) = (float) h; // return still open handle
return;
default: // write
h = Sys_FileOpenWrite (va("%s/%s", com_gamedir, p));
G_FLOAT(OFS_RETURN) = (float) h;
return;
}
}
void PF_close (void)
{
int h = (int)G_FLOAT(OFS_PARM0);
Sys_FileClose(h);
}
void PF_read (void)
{
// reads one line (to a \n) into a string
int h = (int)G_FLOAT(OFS_PARM0);
int test;
char *p;
memset(pr_string_temp, 0, 127);
p = pr_string_temp;
Sys_FileRead(h, p, 1);
while (p && p[0] != '\n')
{
*p++;
test = Sys_FileRead(h, p, 1);
if (p[0] == 13) // carriage return
Sys_FileRead(h, p, 1); // skip
if (!test)
break;
};
p[0] = 0;
if (strlen(pr_string_temp) == 0)
G_INT(OFS_RETURN) = OFS_NULL;
else
G_INT(OFS_RETURN) = pr_string_temp - pr_strings;
}
void PF_write (void)
{
// writes to file, like bprint
float handle = G_FLOAT(OFS_PARM0);
char *str = PF_VarString(1);
Sys_FileWrite (handle, str, strlen(str));
}
// 01-24-2000 FrikaC QuakeC file system End
Phew! Anyway, scroll down to the bottom of the file
and in that big block that is pr_builtin_t, stick these at the end
after PF_setspawnparms
// 01-23-2000 FrikaC QuakeC string manipulation Begin
// FIXME: Register with QSG, make room for QW functions
PF_zone,
PF_unzone,
PF_strlen,
PF_strcat,
PF_substring,
PF_stof,
PF_stov,
// 01-23-2000 FrikaC QuakeC string manipulation End
// 01-24-2000 FrikaC QuakeC file system Begin
PF_open,
PF_close,
PF_read,
PF_write
// 01-24-2000 FrikaC QuakeC file system End
[Be sure to stick a comma on the end of
PF_setspawnparms :]. Right, one last thing. Z_Free which I used in
PF_unzone likes to stop the game with a Sys_Error if the memory you're
trying to free wasn't allocated for the zone. To make my unzone
function actually useful, we must make that error just quietly return.
Open up zone.c and find the function Z_Free, near in the top section of
the function you will see:
if (block->id != ZONEID)
Sys_Error ("Z_Free: freed a pointer without ZONEID");
Not cool. Change it to this:
if (block->id != ZONEID)
{
Con_DPrintf("Z_Free: freed a pointer without ZONEID\n");
return;
}
Okay that's it for the engine code. Since some of my
new code uses a lot of the zone, you might want to increase the default
allocation. The define is in zone.h, it's called DYNAMIC_SIZE. A value
of 1MB (0x100000) should be enough. That is entirely optional though.
On to the QuakeC. First off, you'll need to add this chunk of stuff to your defs.qc file
string(string s) zone = #79;
void(string s) unzone = #80;
float (string s) strlen = #81;
string(string s1, string s2) strcat = #82;
string(string s, float start, float length) substring = #83;
float(string s) stof = #84;
vector(string s) stov = #85;
float(string filename, float mode) open = #86;
void(float handle) close = #87;
string(float handle) read = #88;
void(float handle, string s) write = #89;
Additionally, put these constants somewhere to make the open command a little easier to use:
float FILE_WRITE = 2;
float FILE_APPEND = 1;
float FILE_READ = 0;
Here now is an explanation of each of the commands
that we just slaved over making. (Well, I slaved, you just copied :).
These descriptions should get you started. If they just don't do it for
you, there is also some example code at the bottom.
- open - This is pretty basic, it opens up the file you
specify by the string parameter, and passes you back the file handle
you'll need for all the other file functions. There are three modes
specified by the second parameter: FILE_WRITE, FILE_APPEND and
FILE_READ. Read allows you to only use the read command on this file
handle. Write, well you get the picture. Append will open an existing
file and you can use write to add on to it. Open will return -1 (on
FILE_READ typically, when the file couldn't be found) if the file could
not be opened. Also note that files are always relative to the current
game directory.
Ex: f = open("foo.txt", FILE_READ);
- close - Closes a file opened with the open command.
Quake will close any open files on quit, but I recommend that you close
any and all files you've left open at any opportunity you have in the
code.
Ex: close(f);
- write - Another simple command, pass the file handle
you want to write to in the first parameter, then the string you want
to write. It operates a little like bprint. You can use ftos() and
vtos() to write game data. Note that you must put a \n on the line if
you want to read each line back with read(). You can also use something
similar to the multi-centerprint trick on this function to write
multiple strings at once.
Ex: write(f, "file test example\n");
- read - Read is a lot like INPUT# for anyone that used
to code in BASIC. It returns exactly one line of text from a file. The
function looks for line feeds (\n), so you are somewhat limited in that
you cannot use strings that contain linefeeds. Additionally, carriage
returns (added by many DOS/WIN text editors) are discarded, this is
done so that an unwitting Windows user doesn't screw up the file when
he opens it in notpad.
Ex: h = read(f);
- strlen - Same as the C function of the same name, returns the length, in characters, of the string passed to it.
Ex: size = strlen(h);
- strcat - This is a pretty useful function to have
around, it's exactly what us QC coders have dreamed about for a while:
string culmination. You pass in two stings, and they become one!
Presto! Unfortunately (or fortunately) it's not exactly like the C
function where it gets it's name, the string it creates is stored in a
special buffer (not simply appended, which would be an absolute
nightmare), and will subsequently be overwritten by another strcat
function. This isn't the same buffer as the rest of the functions that
return strings, so you can use strcat on their outputs witout fear of
overwriting their outputs. (more on that later)
Ex: p = strcat(h, " is my favorite variable");
- substring - This is probably most like the substr
function in Perl. Pass to it a string, a start and length then it'll
give a portion of the string back. This is best shown by example:
Ex: substring("Abraham Lincoln", 1, 3) == "bra"
- stof - This is straight from QuakeWorld. It simply
converts a string into a float. No muss, no fuss. This is the
reciprocal of ftos(), you definitely need this for reading file data
back.
Ex: stof("0.09") == 0.09
- stov - My own little invention, similar to stof(). This
converts a string directly into a vector. The string should be in the
format '9.0 0.3 650.0' or something similar. This is the reciprocal of
vtos(). Here is a bad example that tells you nothing:
Ex: v = stov(h);
- zone - There is a reason I left these two functions
(zone and unzone) to the very end on the descriptions: They are very
difficult to explain in layman's terms. Zone essentially copies the
string you pass to it into the "zone", then returns the pointer to it's
new location. This is useful because the pointer string system can be
frustrating in QuakeC sometimes. For instance, it has been impossible
up until now to centerprint more than one value (ftos, vtos), without a
lot hassle with WriteByte. This is because any string created with
those functions (and some of my new functions) are stored in one
solitary string buffer. With this function, you can store the output of
one of these functions in the zone, and it will not be overwritten by
the next function to use the buffer. It also has other advantages. For
instance, many people have tried to use a player's netname for some
type of string input, unfortunately, they soon discover that as the
player changes his name, the variables you thought you copied his name
with have also changed. (It's a fun way to learn about pointers, isn't
it? :) Well zone can be employed to copy off the netname (or any other
variable) and store it for safe keeping. Be careful though, Quake has
only 48k of zone allocated by default, this can quickly fill up with
all the stuff the engine likes to put in there (cvar definitions,
aliases, etc.). You can easily specify more with the -zone commandline
parameter though. This is probably an "advanced" function, the average
coder will probably have no use of it.
Ex: h = zone(h);
- unzone - This simply frees a string defintion from
memory that was created by the zone() command. As the zone is never
cleared inside the game, I recommend that you tidy up anything you zone
or else you will likely run out of zone space before not too long.
Ex: unzone(h);
An example application of all this in QC might help. So I wrote one. Here, read it!
void () saveme =
{
local string h;
local float file;
file = open ("save.txt", FILE_WRITE);
write(file, "// Sample Save File\n");
h = ftos(self.health);
write(file, h);
write(file, "\n");
h = vtos(self.origin);
write(file, h);
write(file, "\n");
h = vtos(self.angles);
write(file, h);
write(file, "\n");
close(file);
};
void () loadme =
{
local string h;
local float file;
local vector v;
file = open ("save.txt", FILE_READ);
if (file == -1)
{
bprint("Error: file not found\n");
return;
}
h = read(file); // reads one line at a time (up to a \n)
// the first line is just a comment, ignore it
h = read(file);
self.health = stof(h);
h = read(file);
v = stov(h);
setorigin(self, v);
h = read(file);
v = stov(h);
self.angles = v;
self.fixangle = TRUE;
close(file);
};
To use this, put this code at the end of world.qc.
Compile, fire it up, then set developer to 1. Use qcexec (your engine
has qcexec, right?) to save and then restore yourself. ("qcexec
saveme"...then walk into another room and type "qcexec loadme").
Well that's it, I hope you enjoyed it, I sure did. Please, if you find any bugs or have any suggestions, don't hesitate to e-mail me. Cya.
|