2 Copyright (c) 2001, Loki software, inc.
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
8 Redistributions of source code must retain the above copyright notice, this list
9 of conditions and the following disclaimer.
11 Redistributions in binary form must reproduce the above copyright notice, this
12 list of conditions and the following disclaimer in the documentation and/or
13 other materials provided with the distribution.
15 Neither the name of Loki software nor the names of its contributors may be used
16 to endorse or promote products derived from this software without specific prior
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23 DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 // - Directories should be searched in the following order: ~/.q3a/baseq3,
35 // install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
41 // Leonardo Zide (leo@lokigames.com)
47 #if defined ( __linux__ ) || defined ( __APPLE__ )
54 #define S_ISDIR( mode ) ( mode & _S_IFDIR )
57 // TTimo: String functions
58 // see http://www.qeradiant.com/faq/index.cgi?file=175
66 #include "unzip-vfspk3.h"
76 // =============================================================================
79 static GSList* g_unzFiles;
80 static GSList* g_pakFiles;
81 static char g_strDirs[VFS_MAXDIRS][PATH_MAX];
83 static bool g_bUsePak = true;
85 // =============================================================================
88 static void vfsAddSlash( char *str ){
89 int n = strlen( str );
91 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
97 static void vfsFixDOSName( char *src ){
104 if ( *src == '\\' ) {
111 static void vfsInitPakFile( const char *filename ){
117 uf = unzOpen( filename );
119 g_FuncTable.m_pfnSysFPrintf( SYS_WRN, " failed to init pak file %s\n", filename );
122 g_FuncTable.m_pfnSysPrintf( " pak file: %s\n", filename );
124 g_unzFiles = g_slist_append( g_unzFiles, uf );
126 err = unzGetGlobalInfo( uf,&gi );
127 if ( err != UNZ_OK ) {
130 unzGoToFirstFile( uf );
132 for ( i = 0; i < gi.number_entry; i++ )
134 char filename_inzip[NAME_MAX];
135 unz_file_info file_info;
138 err = unzGetCurrentFileInfo( uf, &file_info, filename_inzip, sizeof( filename_inzip ), NULL, 0, NULL, 0 );
139 if ( err != UNZ_OK ) {
143 file = (VFS_PAKFILE*)g_malloc( sizeof( VFS_PAKFILE ) );
144 g_pakFiles = g_slist_append( g_pakFiles, file );
146 vfsFixDOSName( filename_inzip );
147 g_strdown( filename_inzip );
149 file->name = g_strdup( filename_inzip );
150 file->size = file_info.uncompressed_size;
152 memcpy( &file->zipinfo, uf, sizeof( unz_s ) );
154 if ( ( i + 1 ) < gi.number_entry ) {
155 err = unzGoToNextFile( uf );
156 if ( err != UNZ_OK ) {
163 static GSList* vfsGetListInternal( const char *refdir, const char *ext, bool directories ){
164 GSList *lst, *lst_aux, *files = NULL;
165 char dirname[NAME_MAX], extension[NAME_MAX], filename[NAME_MAX];
166 char basedir[NAME_MAX];
174 if ( refdir != NULL ) {
175 strcpy( dirname, refdir );
176 g_strdown( dirname );
177 vfsFixDOSName( dirname );
178 vfsAddSlash( dirname );
183 dirlen = strlen( dirname );
186 strcpy( extension, ext );
191 g_strdown( extension );
193 for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
195 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
196 gboolean found = FALSE;
199 // check that the file name begins with dirname
200 for ( i = 0; ( *ptr && i < dirlen ); i++, ptr++ )
201 if ( *ptr != dirname[i] ) {
210 char *sep = strchr( ptr, '/' );
217 // check for duplicates
218 for ( lst_aux = files; lst_aux; lst_aux = g_slist_next( lst_aux ) )
219 if ( strncmp( (char*)lst_aux->data, ptr, i ) == 0 ) {
225 char *name = g_strndup( ptr, i + 1 );
227 files = g_slist_append( files, name );
233 char *ptr_ext = strrchr( ptr, '.' );
234 if ( ( ext != NULL ) && ( ( ptr_ext == NULL ) || ( strcmp( ptr_ext + 1, extension ) != 0 ) ) ) {
238 // check for duplicates
239 for ( lst_aux = files; lst_aux; lst_aux = g_slist_next( lst_aux ) )
240 if ( strcmp( (char*)lst_aux->data, ptr ) == 0 ) {
246 files = g_slist_append( files, g_strdup( ptr ) );
251 for ( i = 0; i < g_numDirs; i++ )
253 strcpy( basedir, g_strDirs[i] );
254 strcat( basedir, dirname );
256 diskdir = g_dir_open( basedir, 0, NULL );
258 if ( diskdir != NULL ) {
261 const char* name = g_dir_read_name( diskdir );
262 if ( name == NULL ) {
266 if ( directories && ( name[0] == '.' ) ) {
270 sprintf( filename, "%s%s", basedir, name );
271 stat( filename, &st );
273 if ( ( S_ISDIR( st.st_mode ) != 0 ) != directories ) {
277 gboolean found = FALSE;
279 dirlist = g_strdup( name );
281 g_strdown( dirlist );
283 char *ptr_ext = strrchr( dirlist, '.' );
285 || ( ext != NULL && ptr_ext != NULL && ptr_ext[0] != '\0' && strcmp( ptr_ext + 1, extension ) == 0 ) ) {
287 // check for duplicates
288 for ( lst_aux = files; lst_aux; lst_aux = g_slist_next( lst_aux ) )
289 if ( strcmp( (char*)lst_aux->data, dirlist ) == 0 ) {
295 files = g_slist_append( files, g_strdup( dirlist ) );
301 g_dir_close( diskdir );
309 This behaves identically to -stricmp(a,b), except that ASCII chars
310 [\]^`_ come AFTER alphabet chars instead of before. This is because
311 it effectively converts all alphabet chars to uppercase before comparison,
312 while stricmp converts them to lowercase.
314 //!\todo Analyse the code in rtcw/q3 to see how it behaves.
315 static int vfsPakSort( const void *a, const void *b ){
326 if ( c1 >= 'a' && c1 <= 'z' ) {
329 if ( c2 >= 'a' && c2 <= 'z' ) {
333 if ( c1 == '\\' || c1 == ':' ) {
336 if ( c2 == '\\' || c2 == ':' ) {
340 // Arnout: note - sort pakfiles in reverse order. This ensures that
341 // later pakfiles override earlier ones. This because the vfs module
342 // returns a filehandle to the first file it can find (while it should
343 // return the filehandle to the file in the most overriding pakfile, the
344 // last one in the list that is).
346 //return -1; // strings not equal
347 return 1; // strings not equal
355 return 0; // strings are equal
358 // =============================================================================
361 // reads all pak files from a dir
363 The gamemode hacks in here will do undefined things with files called zz_*.
364 This is simple to fix by cleaning up the hacks, but may be better left alone
365 if the engine code does the same thing.
367 void vfsInitDirectory( const char *path ){
368 char filename[PATH_MAX];
370 GSList *dirlist = NULL;
371 int iGameMode; // 0: no filtering 1: SP 2: MP
373 if ( g_numDirs == ( VFS_MAXDIRS - 1 ) ) {
377 // See if we are in "sp" or "mp" mapping mode
378 const char* gamemode = g_FuncTable.m_pfnReadProjectKey( "gamemode" );
381 if ( strcmp( gamemode, "sp" ) == 0 ) {
384 else if ( strcmp( gamemode, "mp" ) == 0 ) {
395 strcpy( g_strDirs[g_numDirs], path );
396 vfsFixDOSName( g_strDirs[g_numDirs] );
397 vfsAddSlash( g_strDirs[g_numDirs] );
401 dir = g_dir_open( path, 0, NULL );
404 g_FuncTable.m_pfnSysPrintf( "vfs directory: %s\n", path );
408 const char* name = g_dir_read_name( dir );
409 if ( name == NULL ) {
413 char *ext = (char*)strrchr( name, '.' );
414 if ( ( ext == NULL ) || ( strcasecmp( ext, ".pk3" ) != 0 ) ) {
418 char* direntry = g_strdup( name );
420 // using the same kludge as in engine to ensure consistency
424 if ( strncmp( direntry,"sp_",3 ) == 0 ) {
425 memcpy( direntry,"zz",2 );
429 if ( strncmp( direntry,"mp_",3 ) == 0 ) {
430 memcpy( direntry,"zz",2 );
435 dirlist = g_slist_append( dirlist, direntry );
441 dirlist = g_slist_sort( dirlist, vfsPakSort );
443 // add the entries to the vfs and free the list
446 GSList *cur = dirlist;
447 char* name = (char*)cur->data;
452 if ( strncmp( name,"mp_",3 ) == 0 ) {
454 dirlist = g_slist_remove( cur, name );
457 else if ( strncmp( name,"zz_",3 ) == 0 ) {
458 memcpy( name,"sp",2 );
462 if ( strncmp( name,"sp_",3 ) == 0 ) {
464 dirlist = g_slist_remove( cur, name );
467 else if ( strncmp( name,"zz_",3 ) == 0 ) {
468 memcpy( name,"mp",2 );
473 sprintf( filename, "%s/%s", path, name );
474 vfsInitPakFile( filename );
477 dirlist = g_slist_remove( cur, name );
481 g_FuncTable.m_pfnSysFPrintf( SYS_WRN, "vfs directory not found: %s\n", path );
486 // frees all memory that we allocated
487 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
488 // (for instance when modifying the project settings)
492 unzClose( (unzFile)g_unzFiles->data );
493 g_unzFiles = g_slist_remove( g_unzFiles, g_unzFiles->data );
496 // avoid dangling pointer operation (makes BC hangry)
497 GSList *cur = g_pakFiles;
502 VFS_PAKFILE* file = (VFS_PAKFILE*)cur->data;
503 g_free( file->name );
505 next = g_slist_remove( cur, file );
510 void vfsFreeFile( void *p ){
514 GSList* vfsGetFileList( const char *dir, const char *ext ){
515 return vfsGetListInternal( dir, ext, false );
518 GSList* vfsGetDirList( const char *dir ){
519 return vfsGetListInternal( dir, NULL, true );
522 void vfsClearFileDirList( GSList **lst ){
525 g_free( ( *lst )->data );
526 *lst = g_slist_remove( *lst, ( *lst )->data );
530 int vfsGetFileCount( const char *filename, int flag ){
532 char fixed[NAME_MAX], tmp[NAME_MAX];
535 strcpy( fixed, filename );
536 vfsFixDOSName( fixed );
539 if ( !flag || ( flag & VFS_SEARCH_PAK ) ) {
540 for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
542 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
544 if ( strcmp( file->name, fixed ) == 0 ) {
550 if ( !flag || ( flag & VFS_SEARCH_DIR ) ) {
551 for ( i = 0; i < g_numDirs; i++ )
553 strcpy( tmp, g_strDirs[i] );
554 strcat( tmp, fixed );
555 if ( access( tmp, R_OK ) == 0 ) {
564 // open a full path file
565 int vfsLoadFullPathFile( const char *filename, void **bufferptr ){
569 f = fopen( filename, "rb" );
574 fseek( f, 0, SEEK_END );
578 *bufferptr = g_malloc( len + 1 );
579 if ( *bufferptr == NULL ) {
583 fread( *bufferptr, 1, len, f );
586 // we need to end the buffer with a 0
587 ( (char*) ( *bufferptr ) )[len] = 0;
592 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
593 int vfsLoadFile( const char *filename, void **bufferptr, int index ){
595 char tmp[NAME_MAX], fixed[NAME_MAX];
599 strcpy( fixed, filename );
600 vfsFixDOSName( fixed );
603 for ( i = 0; i < g_numDirs; i++ )
605 strcpy( tmp, g_strDirs[i] );
606 strcat( tmp, filename );
607 if ( access( tmp, R_OK ) == 0 ) {
608 if ( count == index ) {
609 return vfsLoadFullPathFile( tmp,bufferptr );
616 for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
618 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
620 if ( strcmp( file->name, fixed ) != 0 ) {
624 if ( count == index ) {
625 memcpy( file->zipfile, &file->zipinfo, sizeof( unz_s ) );
627 if ( unzOpenCurrentFile( file->zipfile ) != UNZ_OK ) {
631 *bufferptr = g_malloc( file->size + 1 );
632 // we need to end the buffer with a 0
633 ( (char*) ( *bufferptr ) )[file->size] = 0;
635 i = unzReadCurrentFile( file->zipfile, *bufferptr, file->size );
636 unzCloseCurrentFile( file->zipfile );
657 \param shorten will try to match against the short version
658 recent switch back to short path names in project settings has broken some stuff
659 with shorten == true, we will convert in to short version before looking for root
660 FIXME WAAA .. the stuff below is much more simple on linux .. add appropriate #ifdef
662 char* vfsExtractRelativePath_short( const char *in, bool shorten ){
665 char check[PATH_MAX];
666 static char out[PATH_MAX];
670 Sys_Printf( "vfsExtractRelativePath: %s\n", in );
676 if ( GetShortPathName( in, l_in, PATH_MAX ) == 0 ) {
678 Sys_Printf( "GetShortPathName failed\n" );
687 vfsCleanFileName( l_in );
690 vfsCleanFileName( l_in );
691 #endif // ifdef WIN32
695 Sys_Printf( "cleaned path: %s\n", l_in );
698 for ( i = 0; i < g_numDirs; i++ )
700 strcpy( check,g_strDirs[i] );
701 vfsCleanFileName( check );
703 Sys_Printf( "Matching against %s\n", check );
706 // try to find a match
707 if ( strstr( l_in, check ) ) {
708 strcpy( out,l_in + strlen( check ) + 1 );
715 Sys_Printf( "vfsExtractRelativePath: success\n" );
720 Sys_Printf( "vfsExtractRelativePath: failed\n" );
726 // FIXME TTimo: this and the above should be merged at some point
727 char* vfsExtractRelativePath( const char *in ){
728 static char out[PATH_MAX];
729 unsigned int i, count;
730 char *chunk, *backup = NULL; // those point to out stuff
731 char *ret = vfsExtractRelativePath_short( in, false );
734 Sys_Printf( "trying with a short version\n" );
736 ret = vfsExtractRelativePath_short( in, true );
738 // ok, but we have a relative short version now
739 // hack the long relative version out of here
741 for ( i = 0; i < strlen( ret ); i++ )
743 if ( ret[i] == '/' ) {
747 // this is the clean, not short version
749 vfsCleanFileName( out );
750 for ( i = 0; i <= count; i++ )
752 chunk = strrchr( out, '/' );
765 void vfsCleanFileName( char *in ){
768 int n = strlen( in );
769 if ( in[n - 1] == '/' ) {
774 // HYDRA: this now searches VFS/PAK files in addition to the filesystem
775 // if FLAG is unspecified then ONLY dirs are searched.
776 // PAK's are searched before DIRs to mimic engine behaviour
777 // index is ignored when searching PAK files.
779 char* vfsGetFullPath( const char *in, int index, int flag ){
781 static char out[PATH_MAX];
785 if ( flag & VFS_SEARCH_PAK ) {
786 char fixed[NAME_MAX];
790 vfsFixDOSName( fixed );
793 for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
795 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
798 lastptr = file->name;
800 while ( ( ptr = strchr( lastptr,'/' ) ) != NULL )
803 if ( strcmp( lastptr, fixed ) == 0 ) {
804 strncpy( out,file->name,PATH_MAX );
811 if ( !flag || ( flag & VFS_SEARCH_DIR ) ) {
812 for ( i = 0; i < g_numDirs; i++ )
814 strcpy( tmp, g_strDirs[i] );
816 if ( access( tmp, R_OK ) == 0 ) {
817 if ( count == index ) {
829 // TODO TTimo on linux the base prompt is ~/.q3a/<fs_game>
830 // given the file dialog, we could push the strFSBasePath and ~/.q3a into the directory shortcuts
831 // FIXME TTimo is this really a VFS functionality?
832 // actually .. this should be the decision of the core isn't it?
833 // or .. add an API so that the base prompt can be set during VFS init
834 const char* vfsBasePromptPath(){
836 static const char* path = "C:";
838 static const char* path = "/";