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 LordHavoc. 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(). 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.
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. |