8dadfb4878
Cmake: Add FTE_WERROR option, defaults to true in debug builds and off in release builds (in case future compilers have issues). Cmake: Pull in libXscreensaver so we don't get interrupted by screensavers when playing demos. Make: Added `make webcl-rel` for a web build without server bloat (eg for sites focused on demo playback. Yes, this means you XantoM). fteqcc: Include the decompiler in fteqcc (non-gui) builds ('-d' arg). fteqcc: Decompiler can now mostly handle hexen2 mods without any unknown opcodes. Allow ezHud and OpenSSL to be compiled as in-engine plugins, potentially for web and windows ports respectively. Web: Fix support for ogg vorbis. Add support for voip. Web: Added basic support for WebXR. QTV: Don't try seeking on unseekable qtv streams. Don't spam when developer 1 is set. QTV: add support for some eztv extensions. MVD: added hack to use ktx's vweps in mvd where mvdsv doesn't bother to record the info. qwfwd: hack around a hack in qwfwd, allowing it to work again. recording: favour qwd in single player, instead of mvd. Protocol: reduce client memory used for precache names. Bump maximum precache counts - some people are just abusive, yes you Orl. hexen2: add enough clientside protocol compat to play the demo included with h2mp. lacks effects. in_xflip: restored this setting. fs_hidesyspaths: new cvar, defaults to enabled so you won't find your username or whatever turning up in screenshots or the like. change it to 0 before debuging stuff eg via 'path'. gl_overbright_models: Added cvar to match QS. netchan: Added MTU determination, we'll no longer fail to connect when routers stupidly drop icmp packets. Win: try a few other versions of xinput too. CSQC: Added a CSQC_GenerateMaterial function, to give the csqc a chance to generate custom materials. MenuQC: Added support for the skeletal objects API.
553 lines
14 KiB
C
553 lines
14 KiB
C
#include "../plugin.h"
|
|
#include "../../engine/common/fs.h"
|
|
|
|
plugfsfuncs_t *filefuncs;
|
|
|
|
static plugthreadfuncs_t *threading;
|
|
#define Sys_CreateMutex() (threading?threading->CreateMutex():NULL)
|
|
#define Sys_LockMutex(m) (threading?threading->LockMutex(m):true)
|
|
#define Sys_UnlockMutex if(threading)threading->UnlockMutex
|
|
#define Sys_DestroyMutex if(threading)threading->DestroyMutex
|
|
|
|
|
|
|
|
//
|
|
// in memory
|
|
//
|
|
|
|
typedef struct
|
|
{
|
|
fsbucket_t bucket;
|
|
|
|
char name[MAX_QPATH];
|
|
const struct dvpkfile_s *file;
|
|
} mvpkfile_t;
|
|
|
|
typedef struct vpk_s
|
|
{
|
|
searchpathfuncs_t pub;
|
|
char descname[MAX_OSPATH];
|
|
|
|
qbyte *treedata; //raw file list
|
|
size_t treesize;
|
|
|
|
struct vpk_s **fragments;
|
|
size_t numfragments;
|
|
|
|
void *mutex;
|
|
vfsfile_t *handle;
|
|
unsigned int filepos; //the pos the subfiles left it at (to optimize calls to vfs_seek)
|
|
int references; //seeing as all vfiles from a pak file use the parent's vfsfile, we need to keep the parent open until all subfiles are closed.
|
|
|
|
size_t numfiles;
|
|
mvpkfile_t files[1]; //processed file list
|
|
} vpk_t;
|
|
|
|
//
|
|
// on disk
|
|
//
|
|
typedef struct dvpkfile_s
|
|
{ //chars because these are misaligned.
|
|
unsigned char crc[4];
|
|
unsigned char preloadsize[2];
|
|
unsigned char archiveindex[2];
|
|
unsigned char archiveoffset[4];
|
|
unsigned char archivesize[4];
|
|
unsigned char sentinel[2];//=0xffff;
|
|
} dvpkfile_t;
|
|
|
|
typedef struct
|
|
{
|
|
//v1
|
|
unsigned int magic;
|
|
unsigned int version;
|
|
unsigned int tablesize;
|
|
|
|
//v2
|
|
unsigned int filedatasize;
|
|
unsigned int archivemd5size;
|
|
unsigned int globalmd5size;
|
|
unsigned int signaturesize;
|
|
} dvpkheader_t;
|
|
|
|
static void QDECL FSVPK_GetPathDetails(searchpathfuncs_t *handle, char *out, size_t outlen)
|
|
{
|
|
vpk_t *pak = (void*)handle;
|
|
|
|
*out = 0;
|
|
if (pak->references != 1)
|
|
Q_snprintfz(out, outlen, "(%i)", pak->references-1);
|
|
}
|
|
static void QDECL FSVPK_ClosePath(searchpathfuncs_t *handle)
|
|
{
|
|
qboolean stillopen;
|
|
size_t i;
|
|
vpk_t *pak = (void*)handle;
|
|
|
|
if (!Sys_LockMutex(pak->mutex))
|
|
return; //ohnoes
|
|
stillopen = --pak->references > 0;
|
|
Sys_UnlockMutex(pak->mutex);
|
|
if (stillopen)
|
|
return; //not free yet
|
|
|
|
|
|
VFS_CLOSE (pak->handle);
|
|
|
|
Sys_DestroyMutex(pak->mutex);
|
|
for (i = 0; i < pak->numfragments; i++)
|
|
{
|
|
if (pak->fragments[i])
|
|
pak->fragments[i]->pub.ClosePath(&pak->fragments[i]->pub);
|
|
pak->fragments[i] = NULL;
|
|
}
|
|
plugfuncs->Free(pak->fragments);
|
|
plugfuncs->Free(pak->treedata);
|
|
plugfuncs->Free(pak);
|
|
}
|
|
static void QDECL FSVPK_BuildHash(searchpathfuncs_t *handle, int depth, void (QDECL *AddFileHash)(int depth, const char *fname, fsbucket_t *filehandle, void *pathhandle))
|
|
{
|
|
vpk_t *pak = (void*)handle;
|
|
int i;
|
|
|
|
for (i = 0; i < pak->numfiles; i++)
|
|
{
|
|
AddFileHash(depth, pak->files[i].name, &pak->files[i].bucket, &pak->files[i]);
|
|
}
|
|
}
|
|
static unsigned int QDECL FSVPK_FLocate(searchpathfuncs_t *handle, flocation_t *loc, const char *filename, void *hashedresult)
|
|
{
|
|
mvpkfile_t *pf = hashedresult;
|
|
int i;
|
|
vpk_t *pak = (void*)handle;
|
|
|
|
// look through all the pak file elements
|
|
|
|
if (pf)
|
|
{ //is this a pointer to a file in this pak?
|
|
if (pf < pak->files || pf > pak->files + pak->numfiles)
|
|
return FF_NOTFOUND; //was found in a different path
|
|
}
|
|
else
|
|
{
|
|
char fname[MAX_QPATH];
|
|
for (i = 0; i < MAX_QPATH-1 && filename[i]; i++)
|
|
{
|
|
if (filename[i] >= 'A' && filename[i] <= 'Z')
|
|
fname[i] = filename[i] - 'A' + 'a';
|
|
else
|
|
fname[i] = filename[i];
|
|
}
|
|
fname[i] = 0;
|
|
for (i=0 ; i<pak->numfiles ; i++) //look for the file
|
|
{
|
|
if (!strcmp (pak->files[i].name, fname))
|
|
{
|
|
pf = &pak->files[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pf)
|
|
{
|
|
if (loc)
|
|
{
|
|
loc->fhandle = pf;
|
|
*loc->rawname = 0;
|
|
loc->offset = (qofs_t)-1;
|
|
loc->len = ((pf->file->preloadsize[0]<<0)|(pf->file->preloadsize[1]<<8))
|
|
+((pf->file->archivesize[0]<<0)|(pf->file->archivesize[1]<<8)|(pf->file->archivesize[2]<<16)|(pf->file->archivesize[3]<<24));
|
|
}
|
|
return FF_FOUND;
|
|
}
|
|
return FF_NOTFOUND;
|
|
}
|
|
static int QDECL FSVPK_EnumerateFiles (searchpathfuncs_t *handle, const char *match, int (QDECL *func)(const char *, qofs_t, time_t mtime, void *, searchpathfuncs_t *spath), void *parm)
|
|
{
|
|
vpk_t *pak = (void*)handle;
|
|
int num;
|
|
mvpkfile_t *file;
|
|
qofs_t size;
|
|
|
|
for (num = 0; num<(int)pak->numfiles; num++)
|
|
{
|
|
if (filefuncs->WildCmp(match, pak->files[num].name))
|
|
{
|
|
file = &pak->files[num];
|
|
//FIXME: time 0? maybe use the pak's mtime?
|
|
size = ((file->file->preloadsize[0]<<0)|(file->file->preloadsize[1]<<8))
|
|
+((file->file->archivesize[0]<<0)|(file->file->archivesize[1]<<8)|(file->file->archivesize[2]<<16)|(file->file->archivesize[3]<<24));
|
|
if (!func(file->name, size, 0, parm, handle))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int QDECL FSVPK_GeneratePureCRC(searchpathfuncs_t *handle, const int *seed)
|
|
{
|
|
vpk_t *pak = (void*)handle;
|
|
|
|
return filefuncs->BlockChecksum(pak->treedata, pak->treesize);
|
|
}
|
|
|
|
typedef struct {
|
|
vfsfile_t funcs;
|
|
vpk_t *parentpak;
|
|
const qbyte *preloaddata;
|
|
size_t preloadsize;
|
|
qofs_t startpos;
|
|
qofs_t length;
|
|
qofs_t currentpos;
|
|
} vfsvpk_t;
|
|
static int QDECL VFSVPK_ReadBytes (struct vfsfile_s *vfs, void *buffer, int bytestoread)
|
|
{
|
|
vfsvpk_t *vfsp = (void*)vfs;
|
|
int read, preread;
|
|
|
|
if (bytestoread <= 0)
|
|
return 0;
|
|
|
|
if (vfsp->currentpos < vfsp->preloadsize)
|
|
{
|
|
preread = bytestoread;
|
|
if (preread > vfsp->preloadsize-vfsp->currentpos)
|
|
preread = vfsp->preloadsize-vfsp->currentpos;
|
|
if (preread < 0)
|
|
return -1; //erk...
|
|
memcpy(buffer, vfsp->preloaddata+vfsp->currentpos, preread);
|
|
vfsp->currentpos += preread;
|
|
if (preread == bytestoread)
|
|
return preread; //we're done, no need to seek etc
|
|
bytestoread -= preread;
|
|
buffer = (char*)buffer+preread;
|
|
}
|
|
else
|
|
preread = 0;
|
|
|
|
if (vfsp->currentpos + bytestoread > vfsp->length)
|
|
bytestoread = vfsp->length - vfsp->currentpos;
|
|
if (bytestoread <= 0)
|
|
{
|
|
if (preread)
|
|
return preread;
|
|
return -1;
|
|
}
|
|
|
|
if (Sys_LockMutex(vfsp->parentpak->mutex))
|
|
{
|
|
if (vfsp->parentpak->filepos != vfsp->startpos+vfsp->currentpos-vfsp->preloadsize)
|
|
VFS_SEEK(vfsp->parentpak->handle, vfsp->startpos+vfsp->currentpos-vfsp->preloadsize);
|
|
read = VFS_READ(vfsp->parentpak->handle, buffer, bytestoread);
|
|
if (read > 0)
|
|
{
|
|
vfsp->currentpos += read;
|
|
read += preread;
|
|
}
|
|
else if (preread)
|
|
read = preread;
|
|
vfsp->parentpak->filepos = vfsp->startpos+vfsp->currentpos-vfsp->preloadsize;
|
|
Sys_UnlockMutex(vfsp->parentpak->mutex);
|
|
}
|
|
else
|
|
read = preread;
|
|
|
|
return read;
|
|
}
|
|
static int QDECL VFSVPK_WriteBytes (struct vfsfile_s *vfs, const void *buffer, int bytestoread)
|
|
{ //not supported.
|
|
plugfuncs->Error("Cannot write to vpk files\n");
|
|
return 0;
|
|
}
|
|
static qboolean QDECL VFSVPK_Seek (struct vfsfile_s *vfs, qofs_t pos)
|
|
{
|
|
vfsvpk_t *vfsp = (void*)vfs;
|
|
if (pos > vfsp->length)
|
|
return false;
|
|
vfsp->currentpos = pos;// + vfsp->startpos;
|
|
|
|
return true;
|
|
}
|
|
static qofs_t QDECL VFSVPK_Tell (struct vfsfile_s *vfs)
|
|
{
|
|
vfsvpk_t *vfsp = (void*)vfs;
|
|
return vfsp->currentpos;// - vfsp->startpos;
|
|
}
|
|
static qofs_t QDECL VFSVPK_GetLen (struct vfsfile_s *vfs)
|
|
{
|
|
vfsvpk_t *vfsp = (void*)vfs;
|
|
return vfsp->length;
|
|
}
|
|
static qboolean QDECL VFSVPK_Close(vfsfile_t *vfs)
|
|
{
|
|
vfsvpk_t *vfsp = (void*)vfs;
|
|
FSVPK_ClosePath(&vfsp->parentpak->pub); //tell the parent that we don't need it open any more (reference counts)
|
|
plugfuncs->Free(vfsp); //free ourselves.
|
|
return true;
|
|
}
|
|
static vfsfile_t *QDECL FSVPK_OpenVFS(searchpathfuncs_t *handle, flocation_t *loc, const char *mode)
|
|
{
|
|
vpk_t *pack = (void*)handle;
|
|
vfsvpk_t *vfs;
|
|
mvpkfile_t *f = loc->fhandle;
|
|
unsigned int frag;
|
|
|
|
if (strcmp(mode, "rb"))
|
|
return NULL; //urm, unable to write/append
|
|
|
|
frag = (f->file->archiveindex[0]<<0)|(f->file->archiveindex[1]<<8);
|
|
if (frag >= pack->numfragments || !pack->fragments[frag])
|
|
return NULL;
|
|
pack = pack->fragments[frag];
|
|
|
|
vfs = plugfuncs->Malloc(sizeof(vfsvpk_t));
|
|
|
|
vfs->parentpak = pack;
|
|
if (!Sys_LockMutex(pack->mutex))
|
|
{
|
|
plugfuncs->Free(vfs);
|
|
return NULL;
|
|
}
|
|
vfs->parentpak->references++;
|
|
Sys_UnlockMutex(pack->mutex);
|
|
|
|
vfs->preloaddata = (const qbyte*)f->file + sizeof(*f->file);
|
|
vfs->preloadsize = (f->file->preloadsize[0]<<0) | (f->file->preloadsize[1]<<8);
|
|
|
|
vfs->startpos = (f->file->archiveoffset[0]<<0)|(f->file->archiveoffset[1]<<8)|(f->file->archiveoffset[2]<<16)|(f->file->archiveoffset[3]<<24);
|
|
vfs->length = loc->len;
|
|
vfs->currentpos = 0;
|
|
|
|
#ifdef _DEBUG
|
|
{
|
|
mvpkfile_t *pf = loc->fhandle;
|
|
Q_strlcpy(vfs->funcs.dbgname, pf->name, sizeof(vfs->funcs.dbgname));
|
|
}
|
|
#endif
|
|
vfs->funcs.Close = VFSVPK_Close;
|
|
vfs->funcs.GetLen = VFSVPK_GetLen;
|
|
vfs->funcs.ReadBytes = VFSVPK_ReadBytes;
|
|
vfs->funcs.Seek = VFSVPK_Seek;
|
|
vfs->funcs.Tell = VFSVPK_Tell;
|
|
vfs->funcs.WriteBytes = VFSVPK_WriteBytes; //not supported
|
|
|
|
return (vfsfile_t *)vfs;
|
|
}
|
|
|
|
static void QDECL FSVPK_ReadFile(searchpathfuncs_t *handle, flocation_t *loc, char *buffer)
|
|
{
|
|
vfsfile_t *f;
|
|
f = FSVPK_OpenVFS(handle, loc, "rb");
|
|
if (!f) //err...
|
|
return;
|
|
VFS_READ(f, buffer, loc->len);
|
|
VFS_CLOSE(f);
|
|
}
|
|
|
|
static unsigned int FSVPK_WalkTree(vpk_t *vpk, const char *start, const char *end)
|
|
{ //the weird arrangement of these files is presumably an indicator of how source handles its file types.
|
|
const char *ext, *path, *name;
|
|
const dvpkfile_t *file;
|
|
size_t preloadsize;
|
|
unsigned int files = 0;
|
|
while(start < end)
|
|
{ //extensions
|
|
ext = start;
|
|
if (!*ext)
|
|
{
|
|
start++;
|
|
break;
|
|
}
|
|
if (ext[0] == ' ' && !ext[1])
|
|
ext = "";
|
|
start += strlen(start)+1;
|
|
while(start < end)
|
|
{ //paths
|
|
path = start;
|
|
if (!*path)
|
|
{
|
|
start++;
|
|
break;
|
|
}
|
|
if (path[0] == ' ' && !path[1])
|
|
path = "";
|
|
start += strlen(start)+1;
|
|
while(start < end)
|
|
{ //names
|
|
name = start;
|
|
if (!*name)
|
|
{
|
|
start++;
|
|
break;
|
|
}
|
|
if (name[0] == ' ' && !name[1])
|
|
name = "";
|
|
start += strlen(start)+1;
|
|
|
|
file = (const dvpkfile_t*)start;
|
|
preloadsize = (file->preloadsize[0]<<0) | (file->preloadsize[1]<<8);
|
|
start += sizeof(*file)+preloadsize;
|
|
if (start > end)
|
|
return 0; //truncated...
|
|
if (file->sentinel[0] != 0xff || file->sentinel[1] != 0xff)
|
|
return 0; //sentinel failure
|
|
// Con_Printf("Found file %s%s%s%s%s\n", path, *path?"/":"", name, *ext?".":"", ext);
|
|
if (!vpk)
|
|
files++;
|
|
else if (files < vpk->numfiles)
|
|
{
|
|
unsigned int frag = (file->archiveindex[0]<<0)|(file->archiveindex[1]<<8);
|
|
Q_snprintfz(vpk->files[files].name, sizeof(vpk->files[files].name), "%s%s%s%s%s", path, *path?"/":"", name, *ext?".":"", ext);
|
|
filefuncs->CleanUpPath(vpk->files[files].name); //just in case...
|
|
vpk->files[files].file = file;
|
|
|
|
if (vpk->numfragments < frag+1)
|
|
vpk->numfragments = frag+1;
|
|
files++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
COM_LoadPackFile
|
|
|
|
Takes an explicit (not game tree related) path to a pak file.
|
|
|
|
Loads the header and directory, adding the files at the beginning
|
|
of the list so they override previous pack files.
|
|
=================
|
|
*/
|
|
static searchpathfuncs_t *QDECL FSVPK_LoadArchive (vfsfile_t *file, searchpathfuncs_t *parent, const char *filename, const char *desc, const char *prefix)
|
|
{
|
|
dvpkheader_t header;
|
|
int i;
|
|
int numpackfiles;
|
|
vpk_t *vpk, *f;
|
|
vfsfile_t *packhandle;
|
|
int read;
|
|
qbyte *tree;
|
|
unsigned int frag;
|
|
|
|
packhandle = file;
|
|
if (packhandle == NULL)
|
|
return NULL;
|
|
|
|
if (prefix && *prefix)
|
|
return NULL; //not supported at this time
|
|
|
|
read = VFS_READ(packhandle, &header, sizeof(header));
|
|
header.magic = LittleLong(header.magic);
|
|
header.version = LittleLong(header.version);
|
|
header.tablesize = LittleLong(header.tablesize);
|
|
|
|
header.filedatasize = LittleLong(header.filedatasize);
|
|
header.archivemd5size = LittleLong(header.archivemd5size);
|
|
header.globalmd5size = LittleLong(header.globalmd5size);
|
|
header.signaturesize = LittleLong(header.signaturesize);
|
|
|
|
if (read < 12 || header.magic != 0x55aa1234 || header.tablesize <= 0)
|
|
{ //this will include the non-dir files too.
|
|
// Con_Printf("%s is not a vpk\n", desc);
|
|
return NULL;
|
|
}
|
|
i = LittleLong(header.version);
|
|
if (i == 2)
|
|
;//VFS_SEEK(packhandle, 7*sizeof(int));
|
|
else if (i == 1)
|
|
VFS_SEEK(packhandle, 3*sizeof(int));
|
|
else
|
|
{
|
|
Con_Printf("vpk %s is version %x (unspported)\n", desc, i);
|
|
return NULL;
|
|
}
|
|
|
|
tree = plugfuncs->Malloc(header.tablesize);
|
|
read = VFS_READ(packhandle, tree, header.tablesize);
|
|
|
|
numpackfiles = FSVPK_WalkTree(NULL, tree, tree+read);
|
|
|
|
vpk = (vpk_t*)plugfuncs->Malloc (sizeof (*vpk) + sizeof(*vpk->files)*(numpackfiles-1));
|
|
vpk->treedata = tree;
|
|
vpk->treesize = read;
|
|
vpk->numfiles = numpackfiles;
|
|
vpk->numfragments = 0;
|
|
vpk->numfiles = FSVPK_WalkTree(vpk, tree, tree+read);
|
|
|
|
strcpy (vpk->descname, desc);
|
|
vpk->handle = packhandle;
|
|
vpk->filepos = 0;
|
|
VFS_SEEK(packhandle, vpk->filepos);
|
|
|
|
vpk->references++;
|
|
|
|
vpk->mutex = Sys_CreateMutex();
|
|
|
|
Con_Printf ("Added vpkfile %s (%i files)\n", desc, numpackfiles);
|
|
|
|
vpk->pub.fsver = FSVER;
|
|
vpk->pub.GetPathDetails = FSVPK_GetPathDetails;
|
|
vpk->pub.ClosePath = FSVPK_ClosePath;
|
|
vpk->pub.BuildHash = FSVPK_BuildHash;
|
|
vpk->pub.FindFile = FSVPK_FLocate;
|
|
vpk->pub.ReadFile = FSVPK_ReadFile;
|
|
vpk->pub.EnumerateFiles = FSVPK_EnumerateFiles;
|
|
vpk->pub.GeneratePureCRC = FSVPK_GeneratePureCRC;
|
|
vpk->pub.OpenVFS = FSVPK_OpenVFS;
|
|
|
|
vpk->fragments = plugfuncs->Malloc(vpk->numfragments*sizeof(*vpk->fragments));
|
|
for(frag = 0; frag < vpk->numfragments; frag++)
|
|
{
|
|
flocation_t loc;
|
|
char fragname[MAX_OSPATH], *ext;
|
|
Q_strlcpy(fragname, filename, sizeof(fragname));
|
|
ext = strrchr(fragname, '.');
|
|
if (!ext)
|
|
ext = fragname + strlen(fragname);
|
|
if (ext-fragname>4 && !strncmp(ext-4, "_dir", 4))
|
|
ext-=4;
|
|
Q_snprintfz(ext, sizeof(fragname)-(ext-fragname), "_%03u.vpk", frag);
|
|
if (parent->FindFile(parent, &loc, fragname, NULL) != FF_FOUND)
|
|
continue;
|
|
packhandle = parent->OpenVFS(parent, &loc, "rb");
|
|
if (!packhandle)
|
|
continue;
|
|
|
|
vpk->fragments[frag] = f = (vpk_t*)plugfuncs->Malloc(sizeof(*f));
|
|
// Q_strncpyz(f->descname, splitname, sizeof(f->descname));
|
|
f->handle = packhandle;
|
|
// f->rawsize = VFS_GETLEN(f->raw);
|
|
f->references = 1;
|
|
f->mutex = Sys_CreateMutex();
|
|
f->pub.ClosePath = FSVPK_ClosePath;
|
|
}
|
|
return &vpk->pub;
|
|
}
|
|
|
|
qboolean VPK_Init(void)
|
|
{
|
|
threading = plugfuncs->GetEngineInterface(plugthreadfuncs_name, sizeof(*threading));
|
|
if (!threading)
|
|
return false;
|
|
filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs));
|
|
if (!filefuncs)
|
|
return false;
|
|
|
|
//we can't cope with being closed randomly. files cannot be orphaned safely.
|
|
//so ask the engine to ensure we don't get closed before everything else is.
|
|
plugfuncs->ExportFunction("MayShutdown", NULL);
|
|
|
|
if (!plugfuncs->ExportFunction("FS_RegisterArchiveType_vpk", FSVPK_LoadArchive))
|
|
{
|
|
Con_Printf("hl2: Engine doesn't support filesystem plugins\n");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|