mirror of
https://github.com/Garux/netradiant-custom.git
synced 2026-09-29 00:00:03 +02:00
pk3dir/dpkdir directories were working, not pk3/dpk archives because of wrong extension comparison.
994 lines
26 KiB
C++
994 lines
26 KiB
C++
/*
|
|
Copyright (c) 2001, Loki software, inc.
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without modification,
|
|
are permitted provided that the following conditions are met:
|
|
|
|
Redistributions of source code must retain the above copyright notice, this list
|
|
of conditions and the following disclaimer.
|
|
|
|
Redistributions in binary form must reproduce the above copyright notice, this
|
|
list of conditions and the following disclaimer in the documentation and/or
|
|
other materials provided with the distribution.
|
|
|
|
Neither the name of Loki software nor the names of its contributors may be used
|
|
to endorse or promote products derived from this software without specific prior
|
|
written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
|
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
//
|
|
// Rules:
|
|
//
|
|
// - Directories should be searched in the following order: ~/.q3a/baseq3,
|
|
// install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
|
|
//
|
|
// - Pak files are searched first inside the directories.
|
|
// - Case insensitive.
|
|
// - Unix-style slashes (/) (windows is backwards .. everyone knows that)
|
|
//
|
|
// Leonardo Zide (leo@lokigames.com)
|
|
//
|
|
|
|
#include "vfs.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <glib.h>
|
|
|
|
#include "qerplugin.h"
|
|
#include "idatastream.h"
|
|
#include "iarchive.h"
|
|
ArchiveModules& FileSystemQ3API_getArchiveModules();
|
|
#include "ifilesystem.h"
|
|
|
|
#include "generic/callback.h"
|
|
#include "string/string.h"
|
|
#include "stream/stringstream.h"
|
|
#include "os/path.h"
|
|
#include "moduleobservers.h"
|
|
#include "filematch.h"
|
|
|
|
|
|
#define VFS_MAXDIRS 64
|
|
|
|
#if defined( WIN32 )
|
|
#define PATH_MAX 260
|
|
#endif
|
|
|
|
#define gamemode_get GlobalRadiant().getGameMode
|
|
|
|
|
|
|
|
// =============================================================================
|
|
// Global variables
|
|
|
|
Archive* OpenArchive( const char* name );
|
|
|
|
struct archive_entry_t
|
|
{
|
|
CopiedString name;
|
|
Archive* archive;
|
|
bool is_pakfile;
|
|
};
|
|
|
|
#include <list>
|
|
#include <map>
|
|
|
|
typedef std::list<archive_entry_t> archives_t;
|
|
|
|
static archives_t g_archives;
|
|
static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
|
|
static int g_numDirs;
|
|
static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
|
|
static int g_numForbiddenDirs = 0;
|
|
static bool g_bUsePak = true;
|
|
|
|
ModuleObservers g_observers;
|
|
|
|
// =============================================================================
|
|
// Static functions
|
|
|
|
static void AddSlash( char *str ){
|
|
std::size_t n = strlen( str );
|
|
if ( n > 0 ) {
|
|
if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
|
|
globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
|
|
strcat( str, "/" );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void FixDOSName( char *src ){
|
|
if ( src == 0 || strchr( src, '\\' ) == 0 ) {
|
|
return;
|
|
}
|
|
|
|
globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
|
|
|
|
while ( *src )
|
|
{
|
|
if ( *src == '\\' ) {
|
|
*src = '/';
|
|
}
|
|
src++;
|
|
}
|
|
}
|
|
|
|
const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
|
|
StringOutputStream tmp( 16 );
|
|
tmp << LowerCase( ext );
|
|
return archiveModules.findModule( tmp.c_str() );
|
|
}
|
|
|
|
static Archive* InitPakFile( ArchiveModules& archiveModules, const char *filename ){
|
|
const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
|
|
|
|
if ( table != 0 ) {
|
|
archive_entry_t entry;
|
|
entry.name = filename;
|
|
|
|
entry.archive = table->m_pfnOpenArchive( filename );
|
|
entry.is_pakfile = true;
|
|
g_archives.push_back( entry );
|
|
globalOutputStream() << " pak file: " << filename << "\n";
|
|
|
|
return entry.archive;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){
|
|
if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) {
|
|
pathlist = g_slist_prepend( pathlist, path );
|
|
}
|
|
else
|
|
{
|
|
g_free( path );
|
|
}
|
|
}
|
|
|
|
class DirectoryListVisitor : public Archive::Visitor
|
|
{
|
|
GSList*& m_matches;
|
|
const char* m_directory;
|
|
public:
|
|
DirectoryListVisitor( GSList*& matches, const char* directory )
|
|
: m_matches( matches ), m_directory( directory )
|
|
{}
|
|
void visit( const char* name ){
|
|
const char* subname = path_make_relative( name, m_directory );
|
|
if ( subname != name ) {
|
|
if ( subname[0] == '/' ) {
|
|
++subname;
|
|
}
|
|
char* dir = g_strdup( subname );
|
|
char* last_char = dir + strlen( dir );
|
|
if ( last_char != dir && *( --last_char ) == '/' ) {
|
|
*last_char = '\0';
|
|
}
|
|
pathlist_prepend_unique( m_matches, dir );
|
|
}
|
|
}
|
|
};
|
|
|
|
class FileListVisitor : public Archive::Visitor
|
|
{
|
|
GSList*& m_matches;
|
|
const char* m_directory;
|
|
const char* m_extension;
|
|
public:
|
|
FileListVisitor( GSList*& matches, const char* directory, const char* extension )
|
|
: m_matches( matches ), m_directory( directory ), m_extension( extension )
|
|
{}
|
|
void visit( const char* name ){
|
|
const char* subname = path_make_relative( name, m_directory );
|
|
if ( subname != name ) {
|
|
if ( subname[0] == '/' ) {
|
|
++subname;
|
|
}
|
|
if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
|
|
pathlist_prepend_unique( m_matches, g_strdup( subname ) );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
|
|
GSList* files = 0;
|
|
|
|
ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
|
|
|
|
if ( directories ) {
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
DirectoryListVisitor visitor( files, refdir );
|
|
( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
FileListVisitor visitor( files, refdir, ext );
|
|
( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
|
|
}
|
|
}
|
|
|
|
files = g_slist_reverse( files );
|
|
|
|
return files;
|
|
}
|
|
|
|
inline int ascii_to_upper( int c ){
|
|
if ( c >= 'a' && c <= 'z' ) {
|
|
return c - ( 'a' - 'A' );
|
|
}
|
|
return c;
|
|
}
|
|
|
|
/*!
|
|
This behaves identically to stricmp(a,b), except that ASCII chars
|
|
[\]^`_ come AFTER alphabet chars instead of before. This is because
|
|
it converts all alphabet chars to uppercase before comparison,
|
|
while stricmp converts them to lowercase.
|
|
*/
|
|
static int string_compare_nocase_upper( const char* a, const char* b ){
|
|
for (;; )
|
|
{
|
|
int c1 = ascii_to_upper( *a++ );
|
|
int c2 = ascii_to_upper( *b++ );
|
|
|
|
if ( c1 < c2 ) {
|
|
return -1; // a < b
|
|
}
|
|
if ( c1 > c2 ) {
|
|
return 1; // a > b
|
|
}
|
|
if ( c1 == 0 ) {
|
|
return 0; // a == b
|
|
}
|
|
}
|
|
}
|
|
|
|
// Arnout: note - sort pakfiles in reverse order. This ensures that
|
|
// later pakfiles override earlier ones. This because the vfs module
|
|
// returns a filehandle to the first file it can find (while it should
|
|
// return the filehandle to the file in the most overriding pakfile, the
|
|
// last one in the list that is).
|
|
|
|
//!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
|
|
class PakLess
|
|
{
|
|
public:
|
|
bool operator()( const CopiedString& self, const CopiedString& other ) const {
|
|
return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
|
|
}
|
|
};
|
|
|
|
typedef std::set<CopiedString, PakLess> Archives;
|
|
|
|
Archive* AddPk3Dir( const char* fullpath ){
|
|
if ( g_numDirs == VFS_MAXDIRS ) return 0;
|
|
|
|
strncpy( g_strDirs[g_numDirs], fullpath, PATH_MAX );
|
|
g_strDirs[g_numDirs][PATH_MAX] = '\0';
|
|
g_numDirs++;
|
|
|
|
{
|
|
archive_entry_t entry;
|
|
entry.name = fullpath;
|
|
entry.archive = OpenArchive( fullpath );
|
|
entry.is_pakfile = false;
|
|
g_archives.push_back( entry );
|
|
|
|
return entry.archive;
|
|
}
|
|
}
|
|
|
|
// for Daemon DPK vfs
|
|
|
|
Archive* AddDpkDir( const char* fullpath ){
|
|
return AddPk3Dir( fullpath );
|
|
}
|
|
|
|
struct pakfile_path_t
|
|
{
|
|
CopiedString fullpath; // full pak dir or pk3dir name
|
|
bool is_pakfile; // defines is it .pk3dir or .pk3 file
|
|
};
|
|
|
|
typedef std::pair<CopiedString, pakfile_path_t> PakfilePathsKV;
|
|
typedef std::map<CopiedString, pakfile_path_t> PakfilePaths; // key must have no extension, only name
|
|
|
|
static PakfilePaths g_pakfile_paths;
|
|
|
|
void AddDpkPak( const char* name, const char* fullpath, bool is_pakfile ){
|
|
pakfile_path_t pakfile_path;
|
|
pakfile_path.fullpath = fullpath;
|
|
pakfile_path.is_pakfile = is_pakfile;
|
|
g_pakfile_paths.insert( PakfilePathsKV( name, pakfile_path ) );
|
|
}
|
|
|
|
// Comparaison function for version numbers
|
|
// Implementation is based on dpkg's version comparison code (verrevcmp() and order())
|
|
// http://anonscm.debian.org/gitweb/?p=dpkg/dpkg.git;a=blob;f=lib/dpkg/version.c;hb=74946af470550a3295e00cf57eca1747215b9311
|
|
static int char_weight(char c){
|
|
if (std::isdigit(c))
|
|
return 0;
|
|
else if (std::isalpha(c))
|
|
return c;
|
|
else if (c == '~')
|
|
return -1;
|
|
else if (c)
|
|
return c + 256;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
static int VersionCmp(const char* a, const char* b){
|
|
while (*a || *b) {
|
|
int firstDiff = 0;
|
|
|
|
while ((*a && !std::isdigit(*a)) || (*b && !std::isdigit(*b))) {
|
|
int ac = char_weight(*a);
|
|
int bc = char_weight(*b);
|
|
|
|
if (ac != bc)
|
|
return ac - bc;
|
|
|
|
a++;
|
|
b++;
|
|
}
|
|
|
|
while (*a == '0')
|
|
a++;
|
|
while (*b == '0')
|
|
b++;
|
|
|
|
while (std::isdigit(*a) && std::isdigit(*b)) {
|
|
if (firstDiff == 0)
|
|
firstDiff = *a - *b;
|
|
a++;
|
|
b++;
|
|
}
|
|
|
|
if (std::isdigit(*a))
|
|
return 1;
|
|
if (std::isdigit(*b))
|
|
return -1;
|
|
if (firstDiff)
|
|
return firstDiff;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// takes name without ext, returns without ext
|
|
static const char* GetLatestDpkPakVersion( const char* name ){
|
|
const char* maxversion = 0;
|
|
const char* result = 0;
|
|
const char* pakname;
|
|
const char* pakversion;
|
|
int namelen = string_length( name );
|
|
|
|
for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i )
|
|
{
|
|
pakname = i->first.c_str();
|
|
if ( strncmp( pakname, name, namelen ) != 0 || pakname[namelen] != '_' ) continue;
|
|
pakversion = pakname + (namelen + 1);
|
|
if ( maxversion == 0 || VersionCmp( pakversion, maxversion ) > 0 ){
|
|
maxversion = pakversion;
|
|
result = pakname;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// release string after using
|
|
static char* GetCurrentMapDpkPakName(){
|
|
char* mapdir;
|
|
char* mapname;
|
|
int mapnamelen;
|
|
char* result = 0;
|
|
|
|
mapname = string_clone( GlobalRadiant().getMapName() );
|
|
mapnamelen = string_length( mapname );
|
|
|
|
mapdir = strrchr( mapname, '/' );
|
|
if ( mapdir ) {
|
|
mapdir -= 12;
|
|
if ( strncmp( mapdir, ".dpkdir/maps/", 13 ) == 0 ) {
|
|
*mapdir = '\0';
|
|
mapdir = strrchr( mapname, '/' );
|
|
if ( mapdir ) mapdir++;
|
|
else mapdir = mapname;
|
|
result = string_clone( mapdir );
|
|
}
|
|
}
|
|
|
|
string_release( mapname, mapnamelen );
|
|
return result;
|
|
|
|
}
|
|
|
|
// prevent loading duplicates or circular references
|
|
static Archives g_loaded_dpk_paks;
|
|
|
|
// actual pak adding on initialise, deferred from InitDirectory
|
|
// Daemon DPK filesystem doesn't need load all paks it finds
|
|
static void LoadDpkPakWithDeps( const char* pakname ){
|
|
const char* und = strrchr( pakname, '_' );
|
|
if ( !und ) pakname = GetLatestDpkPakVersion( pakname );
|
|
if ( !pakname || g_loaded_dpk_paks.find( pakname ) != g_loaded_dpk_paks.end() ) return;
|
|
|
|
PakfilePaths::iterator i = g_pakfile_paths.find( pakname );
|
|
if ( i == g_pakfile_paths.end() ) return;
|
|
|
|
Archive* arc;
|
|
if ( i->second.is_pakfile ){
|
|
arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() );
|
|
} else {
|
|
arc = AddDpkDir( i->second.fullpath.c_str() );
|
|
}
|
|
g_loaded_dpk_paks.insert( pakname );
|
|
|
|
ArchiveTextFile* depsFile = arc->openTextFile( "DEPS" );
|
|
if ( !depsFile ) return;
|
|
|
|
{
|
|
TextLinesInputStream<TextInputStream> istream = depsFile->getInputStream();
|
|
|
|
CopiedString line;
|
|
const char* c;
|
|
const char* p_name;
|
|
const char* p_name_end;
|
|
const char* p_version;
|
|
const char* p_version_end;
|
|
while ( line = istream.readLine(), string_length( line.c_str() ) ) {
|
|
c = line.c_str();
|
|
while ( std::isspace( *c ) && *c != '\0' ) ++c;
|
|
p_name = c;
|
|
while ( !std::isspace( *c ) && *c != '\0' ) ++c;
|
|
p_name_end = c;
|
|
while ( std::isspace( *c ) && *c != '\0' ) ++c;
|
|
p_version = c;
|
|
while ( !std::isspace( *c ) && *c != '\0' ) ++c;
|
|
p_version_end = c;
|
|
|
|
if ( p_name_end - p_name == 0 ) continue;
|
|
if ( p_version_end - p_version == 0 ) {
|
|
const char* p_pakname;
|
|
CopiedString name_final = CopiedString( StringRange( p_name, p_name_end ) );
|
|
p_pakname = GetLatestDpkPakVersion( name_final.c_str() );
|
|
if ( p_pakname != NULL ) {
|
|
LoadDpkPakWithDeps( p_pakname );
|
|
}
|
|
} else {
|
|
int len = ( p_name_end - p_name ) + ( p_version_end - p_version ) + 1;
|
|
char* p_pakname = string_new( len );
|
|
strncpy( p_pakname, p_name, p_name_end - p_name );
|
|
p_pakname[ p_name_end - p_name ] = '\0';
|
|
strcat( p_pakname, "_" );
|
|
strncat( p_pakname, p_version, p_version_end - p_version );
|
|
LoadDpkPakWithDeps( p_pakname );
|
|
string_release( p_pakname, len );
|
|
}
|
|
}
|
|
}
|
|
|
|
depsFile->release();
|
|
}
|
|
|
|
// end for Daemon DPK vfs
|
|
|
|
// =============================================================================
|
|
// Global functions
|
|
|
|
// reads all pak files from a dir
|
|
void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
|
|
int j;
|
|
|
|
g_numForbiddenDirs = 0;
|
|
StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
|
|
for ( j = 0; j < VFS_MAXDIRS; ++j )
|
|
{
|
|
const char *t = st.getToken();
|
|
if ( string_empty( t ) ) {
|
|
break;
|
|
}
|
|
strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
|
|
g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
|
|
++g_numForbiddenDirs;
|
|
}
|
|
|
|
for ( j = 0; j < g_numForbiddenDirs; ++j )
|
|
{
|
|
char* dbuf = g_strdup( directory );
|
|
if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
|
|
dbuf[strlen( dbuf ) - 1] = 0;
|
|
}
|
|
const char *p = strrchr( dbuf, '/' );
|
|
p = ( p ? ( p + 1 ) : dbuf );
|
|
if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
|
|
g_free( dbuf );
|
|
break;
|
|
}
|
|
g_free( dbuf );
|
|
}
|
|
if ( j < g_numForbiddenDirs ) {
|
|
printf( "Directory %s matched by forbidden dirs, removed\n", directory );
|
|
return;
|
|
}
|
|
|
|
if ( g_numDirs == VFS_MAXDIRS ) {
|
|
return;
|
|
}
|
|
|
|
strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
|
|
g_strDirs[g_numDirs][PATH_MAX] = '\0';
|
|
FixDOSName( g_strDirs[g_numDirs] );
|
|
AddSlash( g_strDirs[g_numDirs] );
|
|
|
|
const char* path = g_strDirs[g_numDirs];
|
|
|
|
g_numDirs++;
|
|
|
|
{
|
|
archive_entry_t entry;
|
|
entry.name = path;
|
|
entry.archive = OpenArchive( path );
|
|
entry.is_pakfile = false;
|
|
g_archives.push_back( entry );
|
|
}
|
|
|
|
if ( g_bUsePak ) {
|
|
|
|
GDir* dir = g_dir_open( path, 0, 0 );
|
|
|
|
if ( dir != 0 ) {
|
|
globalOutputStream() << "vfs directory: " << path << "\n";
|
|
|
|
Archives archives;
|
|
Archives archivesOverride;
|
|
const char* ignore_prefix = "";
|
|
const char* override_prefix = "";
|
|
bool is_pk3_vfs, is_pk4_vfs, is_dpk_vfs;
|
|
|
|
is_pk3_vfs = GetArchiveTable( archiveModules, "pk3" );
|
|
is_pk4_vfs = GetArchiveTable( archiveModules, "pk4" );
|
|
is_dpk_vfs = GetArchiveTable( archiveModules, "dpk" );
|
|
|
|
if ( !is_dpk_vfs ) {
|
|
// See if we are in "sp" or "mp" mapping mode
|
|
const char* gamemode = gamemode_get();
|
|
|
|
if ( strcmp( gamemode, "sp" ) == 0 ) {
|
|
ignore_prefix = "mp_";
|
|
override_prefix = "sp_";
|
|
}
|
|
else if ( strcmp( gamemode, "mp" ) == 0 ) {
|
|
ignore_prefix = "sp_";
|
|
override_prefix = "mp_";
|
|
}
|
|
}
|
|
|
|
for (;; )
|
|
{
|
|
const char* name = g_dir_read_name( dir );
|
|
if ( name == 0 ) {
|
|
break;
|
|
}
|
|
|
|
for ( j = 0; j < g_numForbiddenDirs; ++j )
|
|
{
|
|
const char *p = strrchr( name, '/' );
|
|
p = ( p ? ( p + 1 ) : name );
|
|
if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
|
|
break;
|
|
}
|
|
}
|
|
if ( j < g_numForbiddenDirs ) {
|
|
continue;
|
|
}
|
|
|
|
const char *ext = strrchr( name, '.' );
|
|
char tmppath[PATH_MAX];
|
|
|
|
if ( is_dpk_vfs ) {
|
|
if ( !!ext && !string_compare_nocase_upper( ext, ".dpkdir" ) ) {
|
|
snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
|
|
tmppath[PATH_MAX] = '\0';
|
|
FixDOSName( tmppath );
|
|
AddSlash( tmppath );
|
|
AddDpkPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false );
|
|
}
|
|
}
|
|
|
|
if ( is_pk3_vfs || is_pk4_vfs ) {
|
|
if ( !!ext && ( !string_compare_nocase_upper( ext, ".pk3dir" )
|
|
|| !string_compare_nocase_upper( ext, ".pk4dir" ) ) ) {
|
|
snprintf( tmppath, PATH_MAX, "%s%s/", path, name );
|
|
tmppath[PATH_MAX] = '\0';
|
|
FixDOSName( tmppath );
|
|
AddSlash( tmppath );
|
|
AddPk3Dir( tmppath );
|
|
}
|
|
}
|
|
|
|
// GetArchiveTable() needs "pk3" if ext is ".pk3"
|
|
if ( ( ext == 0 ) || *( ext + 1 ) == '\0' || GetArchiveTable( archiveModules, ext + 1 ) == 0 ) {
|
|
continue;
|
|
}
|
|
|
|
// using the same kludge as in engine to ensure consistency
|
|
if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
|
|
continue;
|
|
}
|
|
if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
|
|
if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
|
|
if ( is_dpk_vfs ) {
|
|
archives.insert( name );
|
|
}
|
|
}
|
|
else {
|
|
archivesOverride.insert( name );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
archives.insert( name );
|
|
}
|
|
|
|
g_dir_close( dir );
|
|
|
|
// add the entries to the vfs
|
|
char* fullpath;
|
|
if ( is_dpk_vfs ) {
|
|
for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) {
|
|
const char* name = i->c_str();
|
|
const char* ext = strrchr( name, '.' );
|
|
if ( !string_compare_nocase_upper( ext, ".dpk" ) ) {
|
|
CopiedString name_final = CopiedString( StringRange( name, ext ) );
|
|
fullpath = string_new_concat( path, name );
|
|
AddDpkPak( name_final.c_str(), fullpath, true );
|
|
string_release( fullpath, string_length( fullpath ) );
|
|
}
|
|
}
|
|
}
|
|
if ( is_pk3_vfs || is_pk4_vfs ) {
|
|
for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
|
|
{
|
|
const char* name = i->c_str();
|
|
const char* ext = strrchr( name, '.' );
|
|
if ( !string_compare_nocase_upper( ext, ".pk3" )
|
|
|| !string_compare_nocase_upper( ext, ".pk4" ) ) {
|
|
fullpath = string_new_concat( path, i->c_str() );
|
|
InitPakFile( archiveModules, fullpath );
|
|
string_release( fullpath, string_length( fullpath ) );
|
|
}
|
|
}
|
|
for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
|
|
{
|
|
const char* name = i->c_str();
|
|
const char* ext = strrchr( name, '.' );
|
|
if ( !string_compare_nocase_upper( ext, ".pk3" )
|
|
|| !string_compare_nocase_upper( ext, ".pk4" ) ) {
|
|
fullpath = string_new_concat( path, i->c_str() );
|
|
InitPakFile( archiveModules, fullpath );
|
|
string_release( fullpath, string_length( fullpath ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
globalErrorStream() << "vfs directory not found: " << path << "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
// frees all memory that we allocated
|
|
// FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
|
|
// (for instance when modifying the project settings)
|
|
void Shutdown(){
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
( *i ).archive->release();
|
|
}
|
|
g_archives.clear();
|
|
|
|
g_numDirs = 0;
|
|
g_numForbiddenDirs = 0;
|
|
|
|
g_pakfile_paths.clear();
|
|
g_loaded_dpk_paks.clear();
|
|
}
|
|
|
|
#define VFS_SEARCH_PAK 0x1
|
|
#define VFS_SEARCH_DIR 0x2
|
|
|
|
int GetFileCount( const char *filename, int flag ){
|
|
int count = 0;
|
|
char fixed[PATH_MAX + 1];
|
|
|
|
strncpy( fixed, filename, PATH_MAX );
|
|
fixed[PATH_MAX] = '\0';
|
|
FixDOSName( fixed );
|
|
|
|
if ( !flag ) {
|
|
flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
|
|
}
|
|
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
|
|
|| !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
|
|
if ( ( *i ).archive->containsFile( fixed ) ) {
|
|
++count;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
ArchiveFile* OpenFile( const char* filename ){
|
|
ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
ArchiveFile* file = ( *i ).archive->openFile( filename );
|
|
if ( file != 0 ) {
|
|
return file;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
ArchiveTextFile* OpenTextFile( const char* filename ){
|
|
ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
|
|
if ( file != 0 ) {
|
|
return file;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// NOTE: when loading a file, you have to allocate one extra byte and set it to \0
|
|
std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
|
|
char fixed[PATH_MAX + 1];
|
|
|
|
strncpy( fixed, filename, PATH_MAX );
|
|
fixed[PATH_MAX] = '\0';
|
|
FixDOSName( fixed );
|
|
|
|
ArchiveFile* file = OpenFile( fixed );
|
|
|
|
if ( file != 0 ) {
|
|
*bufferptr = malloc( file->size() + 1 );
|
|
// we need to end the buffer with a 0
|
|
( (char*) ( *bufferptr ) )[file->size()] = 0;
|
|
|
|
std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
|
|
file->release();
|
|
return length;
|
|
}
|
|
|
|
*bufferptr = 0;
|
|
return 0;
|
|
}
|
|
|
|
void FreeFile( void *p ){
|
|
free( p );
|
|
}
|
|
|
|
GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
|
|
return GetListInternal( dir, ext, false, depth );
|
|
}
|
|
|
|
GSList* GetDirList( const char *dir, std::size_t depth ){
|
|
return GetListInternal( dir, 0, true, depth );
|
|
}
|
|
|
|
void ClearFileDirList( GSList **lst ){
|
|
while ( *lst )
|
|
{
|
|
g_free( ( *lst )->data );
|
|
*lst = g_slist_remove( *lst, ( *lst )->data );
|
|
}
|
|
}
|
|
|
|
const char* FindFile( const char* relative ){
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
if ( ( *i ).archive->containsFile( relative ) ) {
|
|
return ( *i ).name.c_str();
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
const char* FindPath( const char* absolute ){
|
|
const char *best = "";
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
|
|
if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
|
|
best = ( *i ).name.c_str();
|
|
}
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
|
|
class Quake3FileSystem : public VirtualFileSystem
|
|
{
|
|
public:
|
|
void initDirectory( const char *path ){
|
|
InitDirectory( path, FileSystemQ3API_getArchiveModules() );
|
|
}
|
|
void initialise(){
|
|
ArchiveModules& archiveModules = FileSystemQ3API_getArchiveModules();
|
|
bool is_dpk_vfs = GetArchiveTable( archiveModules, "dpk" );
|
|
|
|
if ( is_dpk_vfs ) {
|
|
const char* pakname;
|
|
g_loaded_dpk_paks.clear();
|
|
|
|
pakname = GetLatestDpkPakVersion( "tex-common" );
|
|
if (pakname != NULL) {
|
|
LoadDpkPakWithDeps( pakname );
|
|
}
|
|
|
|
// prevent VFS double start, for MapName="" and MapName="unnamed.map"
|
|
if ( string_length( GlobalRadiant().getMapName() ) ){
|
|
// load map's paks from DEPS
|
|
char* mappakname = GetCurrentMapDpkPakName();
|
|
if ( mappakname != NULL ) {
|
|
LoadDpkPakWithDeps( mappakname );
|
|
string_release( mappakname, string_length( mappakname ) );
|
|
}
|
|
}
|
|
|
|
g_pakfile_paths.clear();
|
|
g_loaded_dpk_paks.clear();
|
|
}
|
|
|
|
globalOutputStream() << "filesystem initialised\n";
|
|
g_observers.realise();
|
|
}
|
|
void shutdown(){
|
|
g_observers.unrealise();
|
|
globalOutputStream() << "filesystem shutdown\n";
|
|
Shutdown();
|
|
}
|
|
|
|
int getFileCount( const char *filename, int flags ){
|
|
return GetFileCount( filename, flags );
|
|
}
|
|
ArchiveFile* openFile( const char* filename ){
|
|
return OpenFile( filename );
|
|
}
|
|
ArchiveTextFile* openTextFile( const char* filename ){
|
|
return OpenTextFile( filename );
|
|
}
|
|
std::size_t loadFile( const char *filename, void **buffer ){
|
|
return LoadFile( filename, buffer, 0 );
|
|
}
|
|
void freeFile( void *p ){
|
|
FreeFile( p );
|
|
}
|
|
|
|
void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
|
|
GSList* list = GetDirList( basedir, depth );
|
|
|
|
for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
|
|
{
|
|
callback( reinterpret_cast<const char*>( ( *i ).data ) );
|
|
}
|
|
|
|
ClearFileDirList( &list );
|
|
}
|
|
void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
|
|
GSList* list = GetFileList( basedir, extension, depth );
|
|
|
|
for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
|
|
{
|
|
const char* name = reinterpret_cast<const char*>( ( *i ).data );
|
|
if ( extension_equal( path_get_extension( name ), extension ) ) {
|
|
callback( name );
|
|
}
|
|
}
|
|
|
|
ClearFileDirList( &list );
|
|
}
|
|
GSList* getDirList( const char *basedir ){
|
|
return GetDirList( basedir, 1 );
|
|
}
|
|
GSList* getFileList( const char *basedir, const char *extension ){
|
|
return GetFileList( basedir, extension, 1 );
|
|
}
|
|
void clearFileDirList( GSList **lst ){
|
|
ClearFileDirList( lst );
|
|
}
|
|
|
|
const char* findFile( const char *name ){
|
|
return FindFile( name );
|
|
}
|
|
const char* findRoot( const char *name ){
|
|
return FindPath( name );
|
|
}
|
|
|
|
void attach( ModuleObserver& observer ){
|
|
g_observers.attach( observer );
|
|
}
|
|
void detach( ModuleObserver& observer ){
|
|
g_observers.detach( observer );
|
|
}
|
|
|
|
Archive* getArchive( const char* archiveName, bool pakonly ){
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
if ( pakonly && !( *i ).is_pakfile ) {
|
|
continue;
|
|
}
|
|
|
|
if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
|
|
return ( *i ).archive;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
|
|
if ( reverse ) {
|
|
g_archives.reverse();
|
|
}
|
|
|
|
for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
|
|
{
|
|
if ( pakonly && !( *i ).is_pakfile ) {
|
|
continue;
|
|
}
|
|
|
|
callback( ( *i ).name.c_str() );
|
|
}
|
|
|
|
if ( reverse ) {
|
|
g_archives.reverse();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
Quake3FileSystem g_Quake3FileSystem;
|
|
|
|
VirtualFileSystem& GetFileSystem(){
|
|
return g_Quake3FileSystem;
|
|
}
|
|
|
|
void FileSystem_Init(){
|
|
}
|
|
|
|
void FileSystem_Shutdown(){
|
|
}
|