IFileSystemV009

From Valve Developer Community
Jump to navigation Jump to search

The SteamPipe update and subsequent Half-Life SDK release on Github came with a modified version of the IFileSystem interface that is designed to work with GoldSource's file searching procedures.

It provides a means to use the search paths defined by the engine to find files in the same way that the engine does, as well as allowing new search paths to be added to find content in other directories.

See IFileSystem for information on operations shared between the GoldSource and Source engine versions of the interface.

Path IDs

In GoldSource, the same file can exist in multiple SearchPaths. IFileSystem defines a few different access modes for determining precisely where it should look:

ROOT
The root directory. This is the location of the game's executable. This is added by the launcher, but will be removed when the engine adds its search paths.
BASE
Same as ROOT.
DEFAULTGAME
Default paths. Generally used when searching all paths.
PLATFORM
The game directory's platform directory.
GAME
Game directories, including localization, _addon and _hd.
GAMECONFIG
The directory containing game configuration files. This is the mod directory.
GAMEDOWNLOAD
The _downloads directory for the mod.
GAME_FALLBACK
If the fallback_dir option is provided in liblist.gam, this path ID is also set. Essentially, this enables you to easily specify that another game's content should also be mounted. It will add search paths for its _addon, _hd and localization directories as well. For example, Condition Zero defines its fallback directory to be "cstrike".

SteamPipe directories

The SteamPipe update also introduced a new set of directories that content is to be placed in. These are intended to prevent pollution of default content by custom content, and to prevent pollution of default and manually installed custom content by automatically downloaded content.

Content located in one of these directories will override the content located in the <base dir> and mod directories.

See GoldSource SteamPipe Directories for all directories that are used, and what conditions apply.

Instantiating an IFileSystem

There is only one instance of the IFileSystem interface, located in the filesystem_stdio library (filesystem_steam is obsolete).

Getting the instance takes a few steps (code modified from DMC's source):

#include "interface.h"
#include "FileSystem.h"

CSysModule* g_pFileSystemModule = NULL;
IFileSystem* g_pFileSystem = NULL;

bool LoadFileSystem()
{
	// Determine which filesystem to use.
#if defined ( _WIN32 )
	char *szFsModule = "filesystem_stdio.dll";
#elif defined(OSX)
	char *szFsModule = "filesystem_stdio.dylib";
#elif defined(LINUX)
	char *szFsModule = "filesystem_stdio.so";
#else
#error
#endif


	char szFSDir[ MAX_PATH ];
	szFSDir[ 0 ] = 0;
#ifdef CLIENT_DLL
	if( gEngfuncs.COM_ExpandFilename( szFsModule, szFSDir, sizeof( szFSDir ) ) == FALSE )
	{
		return false;
	}
#else
	//Just use the filename for the server. No COM_ExpandFilename here.
	strcpy( szFSDir, szFsModule );
#endif

	// Get filesystem interface.
	g_pFileSystemModule = Sys_LoadModule( szFSDir );
	assert( g_pFileSystemModule );
	if( !g_pFileSystemModule )
	{
		return false;
	}

	CreateInterfaceFn fileSystemFactory = Sys_GetFactory( g_pFileSystemModule );
	if( !fileSystemFactory )
	{
		return false;
	}

	g_pFileSystem = ( IFileSystem* ) fileSystemFactory( FILESYSTEM_INTERFACE_VERSION, nullptr );
	assert( g_pFileSystem );
	if( !g_pFileSystem )
	{
		return false;
	}

	return true;
}


Let's go over each step:

#include "interface.h"
#include "FileSystem.h"


In order to make use of the interface, its definition must be included. FileSystem.h includes that definition, and uses interface.h.

CSysModule* g_pFileSystemModule = NULL;
IFileSystem* g_pFileSystem = NULL;


These global variables will keep track of the filesystem module and the filesystem itself, respectively.

	// Determine which filesystem to use.
#if defined ( _WIN32 )
	char *szFsModule = "filesystem_stdio.dll";
#elif defined(OSX)
	char *szFsModule = "filesystem_stdio.dylib";
#elif defined(LINUX)
	char *szFsModule = "filesystem_stdio.so";
#else
#error
#endif


Based on the platform that you're compiling the game for, you'll need a specific library name and extension. The pointer szFsModule will point to the correct name.

	char szFSDir[ MAX_PATH ];
	szFSDir[ 0 ] = 0;
#ifdef CLIENT_DLL
	if( gEngfuncs.COM_ExpandFilename( szFsModule, szFSDir, sizeof( szFSDir ) ) == FALSE )
	{
		return false;
	}
#else
	//Just use the filename for the server. No COM_ExpandFilename here.
	strcpy( szFSDir, szFsModule );
#endif


The full name of the library file should now be determined. On the client side, you can use COM_ExpandFilename to get the absolute path of the file. The server does not expose this function (which actually accesses the filesystem itself to get this information), so you'll have to rely on the game using the correct working directory while loading it.

	// Get filesystem interface.
	g_pFileSystemModule = Sys_LoadModule( szFSDir );
	assert( g_pFileSystemModule );
	if( !g_pFileSystemModule )
	{
		return false;
	}


Next we load the filesystem library. The library will already have been loaded and initialized by the engine, so we're really just getting a handle to the library. If this fails, we stop and return false to indicate failure.

	CreateInterfaceFn fileSystemFactory = Sys_GetFactory( g_pFileSystemModule );
	if( !fileSystemFactory )
	{
		return false;
	}


In order to instantiate the filesystem, we need to have the library's CreateInterface function. If we fail to retrieve it, we stop and return false to indicate failure.

	g_pFileSystem = ( IFileSystem* ) fileSystemFactory( FILESYSTEM_INTERFACE_VERSION, nullptr );
	assert( g_pFileSystem );
	if( !g_pFileSystem )
	{
		return false;
	}


Now we instantiate the filesystem by querying the CreateInterface function for the interface version. If this fails, it can only mean that the filesystem isn't exposed for that version. In practice this should never happen, unless a breaking change is made to the interface that requires all users to upgrade, or if the library has been replaced by a third party.

	return true;


If we've reached this point, that means we've successfully instantiated the filesystem. We can now use it for any file operations, add new search paths, etc.

The LoadFileSystem function should be called in the Initialize function in the client library and either the GiveFnptrsToDll or GameDLLInit functions in the server library. Note that using GameDLLInit will require you to submit a "quit" server command in order to shut down the server in the event that the filesystem fails to load.

Using the filesystem

Searching for files

Searching for files is done using the FindFirst, FindNext and FindClose methods.

FileFindHandle_t handle;

const char* pszName = g_pFileSystem->FindFirst( "models/*", &handle, NULL );

if( pszName )
{
	do
	{
		printf( "File %s\n", pszName );
	}
	while( ( pszName = g_pFileSystem->FindNext( handle ) ) );

	g_pFileSystem->FindClose( handle );
}

This will search for all files in models/ in all search paths.

Note the importance of checking if FindFirst returned a valid string. The find handle is only valid if a file has been found. Otherwise, its value will be unspecified and using it will result in undefined behavior.

Also note that since some search paths point to the same directory, the same files will be returned multiple times as each search path is checked. This is the reason why the Options menu lists models multiple times. This shouldn't be a problem when using a specific path ID.

Finally, using '*' as the filename wildcard will cause it to also return the current (".") and parent ("..") directory files. Be aware of this when enumerating all files.

Adding search paths

At some point it may be necessary to add new search paths to the filesystem. There are 2 ways to add paths; the AddSearchPath and AddSearchPathNoWrite methods.

These functions are mostly identical, except the latter will add the path as a read-only path. Any attempt to write files to a read-only path will fail.

The parameters for these methods are fairly simple:

pPath
The path is relative to the game directory; adding "moddir/sound/weapons" as a search path will cause it to search in "Half-Life/moddir/sound/weapons".
pathID
The path ID is a means to group search paths together. Adding a search path to a preexisting path ID will cause it to be searched after all other paths in that path ID. A null path ID will cause it to only be searched if a null path ID is given to a method, or if a method will always search all paths.

Notes

This version of the filesystem was originally designed for Steam (GCF) based content. As such, its interface contains methods to perform Steam specific actions. Since the SteamPipe update, these will no longer do anything.

The RemoveSearchPath method will cause an access violation regardless of which path you give it. This bug has been reported on Half-Life's bug tracker [1].

See also