================================================================
Title         : Tutorial: Implementing the QuakeC file access into the Quake engine
                (including new QuakeC string manipulation functions)
Date          : 2001-11-01
Filename      : FILE_ACCESS_UPDATED.TXT
Author        : Ryan "FrikaC" Smith
                Matthias "Maddes" Buecher
Email Address : kryten@adelphia.net
                maddes@go.to
Homepage      : FrikBot
                http://www.inside3d.com/frikbot/
                Quake Info Pool
                http://www.quake-info-pool.net/
                Quake Standards Group (short QSG)
                http://www.quakesrc.org/
Complexity    : Low
================================================================

This is the updated version of FrikaC's file access tutorial. The builtin
function numbers and names have been changed, so they do not interfere with
already used numbers in QuakeWorld and registered functions of Lord Havoc.
Additionally the string functions are now protected against memory leaks and the
file read function can distinguish between an empty line and the end of the
file.

The builtin functions were added to the QSG standard by using the Enhanced
BuiltIn Function System (EBFS). If you want to add these functions to your own
engine, then you should add the EBFS before.



FrikaC's new functions may lead to very very long strings, so we have to avoid
memory leaks. There's also a potentioal memory leak in id's PF_Varstring().

First we define the maximum size (length + 0-byte for EndOfString) of a temp
string at the top of PR_CMDS.C...

#define PR_MAX_TEMPSTRING 2048	// 2001-10-25 Enhanced temp string handling by Maddes

To fix PF_Varstring() place the following definition before it...

char	pr_varstring_temp[PR_MAX_TEMPSTRING];	// 2001-10-25 Enhanced temp string handling by Maddes

    ... and change PF_Varstring to the following ...

char *PF_VarString (int	first)
{
	int		i;
// 2001-10-25 Enhanced temp string handling by Maddes  start
	int		maxlen;
	char	*add;

	pr_varstring_temp[0] = 0;
	for (i=first ; i<pr_argc ; i++)
	{
		maxlen = PR_MAX_TEMPSTRING - strlen(pr_varstring_temp) - 1;	// -1 is EndOfString
		add = G_STRING((OFS_PARM0+i*3));
		if (maxlen > strlen(add))
		{
			strcat (pr_varstring_temp, add);
		}
		else
		{
			strncat (pr_varstring_temp, add, maxlen);
			pr_varstring_temp[PR_MAX_TEMPSTRING-1] = 0;
			break;	// can stop here
		}
	}
	return pr_varstring_temp;
// 2001-10-25 Enhanced temp string handling by Maddes  end
}

Change the definition of pr_string_temp before PF_ftos() to ...

char	pr_string_temp[PR_MAX_TEMPSTRING];	// 2001-10-25 Enhanced temp string handling by Maddes



Now follows FrikaC's original tutorial, which now incorporates the new function
numbers in conjunction with EBFS and avoids memory leaks in PF_strcat(),
PF_substring() and PF_fgets().
The file read function was also enhanced to distinguish between an empty line
and the end of the file.

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 and 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:

// 2001-09-20 QuakeC string manipulation by FrikaC/Maddes  start
/*
=================
PF_strzone

string strzone (string)
=================
*/
void PF_strzone (void)
{
	char *m, *p;
	m = G_STRING(OFS_PARM0);
	p = Z_Malloc(strlen(m) + 1);
	strcpy(p, m);

	G_INT(OFS_RETURN) = p - pr_strings;
}

/*
=================
PF_strunzone

string strunzone (string)
=================
*/
void PF_strunzone (void)
{
	Z_Free(G_STRING(OFS_PARM0));
	G_INT(OFS_PARM0) = OFS_NULL; // empty the def
};

/*
=================
PF_strlen

float strlen (string)
=================
*/
void PF_strlen (void)
{
	char *p = G_STRING(OFS_PARM0);
	G_FLOAT(OFS_RETURN) = strlen(p);
}

/*
=================
PF_strcat

string strcat (string, string)
=================
*/

void PF_strcat (void)
{
	char *s1, *s2;
	int		maxlen;	// 2001-10-25 Enhanced temp string handling by Maddes

	s1 = G_STRING(OFS_PARM0);
	s2 = PF_VarString(1);

// 2001-10-25 Enhanced temp string handling by Maddes  start
	pr_string_temp[0] = 0;
	if (strlen(s1) < PR_MAX_TEMPSTRING)
	{
		strcpy(pr_string_temp, s1);
	}
	else
	{
		strncpy(pr_string_temp, s1, PR_MAX_TEMPSTRING);
		pr_string_temp[PR_MAX_TEMPSTRING-1] = 0;
	}

	maxlen = PR_MAX_TEMPSTRING - strlen(pr_string_temp) - 1;	// -1 is EndOfString
	if (maxlen > 0)
	{
		if (maxlen > strlen(s2))
		{
			strcat (pr_string_temp, s2);
		}
		else
		{
			strncat (pr_string_temp, s2, maxlen);
			pr_string_temp[PR_MAX_TEMPSTRING-1] = 0;
		}
	}
// 2001-10-25 Enhanced temp string handling by Maddes  end

	G_INT(OFS_RETURN) = pr_string_temp - pr_strings;
}

/*
=================
PF_substring

string substring (string, float, float)
=================
*/
void PF_substring (void)
{
	int		offset, length;
	int		maxoffset;		// 2001-10-25 Enhanced temp string handling by Maddes
	char	*p;

	p = G_STRING(OFS_PARM0);
	offset = (int)G_FLOAT(OFS_PARM1); // for some reason, Quake doesn't like G_INT
	length = (int)G_FLOAT(OFS_PARM2);

	// cap values
	maxoffset = strlen(p);
	if (offset > maxoffset)
	{
		offset = maxoffset;
	}
	if (offset < 0)
		offset = 0;
// 2001-10-25 Enhanced temp string handling by Maddes  start
	if (length >= PR_MAX_TEMPSTRING)
		length = PR_MAX_TEMPSTRING-1;
// 2001-10-25 Enhanced temp string handling by Maddes  end
	if (length < 0)
		length = 0;

	p += offset;
	strncpy(pr_string_temp, p, length);
	pr_string_temp[length]=0;

	G_INT(OFS_RETURN) = pr_string_temp - pr_strings;
}

/*
=================
PF_stof

float stof (string)
=================
*/
// thanks Zoid, taken from QuakeWorld
void PF_stof (void)
{
	char	*s;

	s = G_STRING(OFS_PARM0);
	G_FLOAT(OFS_RETURN) = atof(s);
}

/*
=================
PF_stov

vector stov (string)
=================
*/
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));
}
// 2001-09-20 QuakeC string manipulation by FrikaC/Maddes  end

// 2001-09-20 QuakeC file access by FrikaC/Maddes  start
/*
=================
PF_fopen

float fopen (string,float)
=================
*/
void PF_fopen (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;
	}
}

/*
=================
PF_fclose

void fclose (float)
=================
*/
void PF_fclose (void)
{
	int h = (int)G_FLOAT(OFS_PARM0);
	Sys_FileClose(h);
}

/*
=================
PF_fgets

string fgets (float)
=================
*/
void PF_fgets (void)
{
	// reads one line (up to a \n) into a string
	int		h;
	int		i;
	int		count;
	char	buffer;

	h = (int)G_FLOAT(OFS_PARM0);

	count = Sys_FileRead(h, &buffer, 1);
	if (count && buffer == '\r')	// carriage return
	{
		count = Sys_FileRead(h, &buffer, 1);	// skip
	}
	if (!count)	// EndOfFile
	{
		G_INT(OFS_RETURN) = OFS_NULL;	// void string
		return;
	}

	i = 0;
	while (count && buffer != '\n')
	{
		if (i < PR_MAX_TEMPSTRING-1)	// no place for character in temp string
		{
			pr_string_temp[i++] = buffer;
		}

		// read next character
		count = Sys_FileRead(h, &buffer, 1);
		if (count && buffer == '\r')	// carriage return
		{
			count = Sys_FileRead(h, &buffer, 1);	// skip
		}
	};
	pr_string_temp[i] = 0;

	G_INT(OFS_RETURN) = pr_string_temp - pr_strings;
}

/*
=================
PF_fputs

void fputs (float,string)
=================
*/
void PF_fputs (void)
{
	// writes to file, like bprint
	float handle = G_FLOAT(OFS_PARM0);
	char *str = PF_VarString(1);
	Sys_FileWrite (handle, str, strlen(str));
}
// 2001-09-20 QuakeC file access by FrikaC/Maddes  end


Phew! Anyway, scroll down to the bottom of the file and in that big block that
is pr_ebfs_builtins, stick these at the end

	{  81, "stof", PF_stof },	// 2001-09-20 QuakeC string manipulation by FrikaC/Maddes

// 2001-09-20 QuakeC file access by FrikaC/Maddes  start
	{ 110, "fopen", PF_fopen },
	{ 111, "fclose", PF_fclose },
	{ 112, "fgets", PF_fgets },
	{ 113, "fputs", PF_fputs },
	{   0, "open", PF_fopen },		// 0 indicates that this entry is just for remapping (because of name and number change)
	{   0, "close", PF_fclose },
	{   0, "read", PF_fgets },
	{   0, "write", PF_fputs },
// 2001-09-20 QuakeC file access by FrikaC/Maddes  end

// 2001-09-20 QuakeC string manipulation by FrikaC/Maddes  start
	{ 114, "strlen", PF_strlen },
	{ 115, "strcat", PF_strcat },
	{ 116, "substring", PF_substring },
	{ 117, "stov", PF_stov },
	{ 118, "strzone", PF_strzone },
	{ 119, "strunzone", PF_strunzone },
	{   0, "zone", PF_strzone },		// 0 indicates that this entry is just for remapping (because of name and number change)
	{   0, "unzone", PF_strunzone },
// 2001-09-20 QuakeC string manipulation by FrikaC/Maddes  end


Right, one last thing. Z_Free which I used in PF_strunzone 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 strunzone 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. On to the QuakeC, please read the EBFS
tutorial before you try to use new builtin functions.

First off, you'll need to add this chunk of stuff to your defs.qc file

float(string s) stof = #81;	// 2001-09-20 QuakeC string manipulation by FrikaC
							// taken from QuakeWorld

// 2001-09-20 QuakeC file access by FrikaC  start
float(string filename, float mode) fopen = #110;
void(float fhandle) fclose = #111;
string(float fhandle) fgets = #112;
void(float fhandle, string s) fputs = #113;
// 2001-09-20 QuakeC file access by FrikaC  end

// 2001-09-20 QuakeC string manipulation by FrikaC  start
float(string s) strlen = #114;
string(string s1, string s2) strcat = #115;
string(string s, float start, float length) substring = #116;
vector(string s) stov = #117;
string(string s) strzone = #118;
string(string s) strunzone = #119;
// 2001-09-20 QuakeC string manipulation by FrikaC  end

Additionally, put these constants somewhere to make the open command a little easier to use:

float FILE_READ = 0;
float FILE_APPEND = 1;
float FILE_WRITE = 2;


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.

* fopen - 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 = fopen("foo.txt", FILE_READ);

* fclose - 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: fclose(f);

* fputs - 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 fgets(). You can also use something similar to the multi-
          centerprint trick on this function to write multiple strings at once.

          Ex: fputs(f, "file test example\n");

* fgets - 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.
          For EndOfFile it returns a VOID string, which you can check with
          "if (line)" in QuakeC. Note that the VOID string is *not* the same as
          an empty string ("").
          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 = fgets(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);

* strzone - There is a reason I left these two functions (strzone and strunzone) to the
            very end on the descriptions: They are very difficult to explain in
            layman's terms. strzone 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 strzone 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 = strzone(h);

* strunzone - This simply frees a string defintion from memory that was created by
              the strzone() command. As the zone is never cleared inside the game, I
              recommend that you tidy up anything you zoned or else you will likely
              run out of zone space before not too long.

              Ex: strunzone(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);
	fputs(file, "// Sample Save File\n");
	h = ftos(self.health);
	fputs(file, h);
	fputs(file, "\n");
	h = vtos(self.origin);
	fputs(file, h);
	fputs(file, "\n");
	h = vtos(self.angles);
	fputs(file, h);
	fputs(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 = fgets(file); // reads one line at a time (up to a \n)
	// the first line is just a comment, ignore it
	h = fgets(file);
	self.health = stof(h);
	h = fgets(file);
	v = stov(h);
	setorigin(self, v);
	h = fgets(file);
	v = stov(h);
	self.angles = v;
	self.fixangle = TRUE;
	close(file);
};

void() listfile =
{
	local float file;
	local float i;
	local string lineno;
	local string line;

	file = fopen ("foo.txt", FILE_READ);
	if (file == -1)
	{
		dprint("Error: file not found\n");
		return;
	}

	i = 0;
	line = fgets(file); // reads one line at a time (up to a \n)
	while(line)
	{
		line = strzone(line);

		i = i + 1;
		lineno = ftos(i);

		dprint(lineno);
		dprint(": ");
		dprint(line);
		dprint("\n");

		line = strunzone(line);

		line = fgets(file);
	}
	dprint("[EOF]\n");

	fclose(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.
