Browse Source

filesystem: make generic archive loading functions (with the exception of WADs). Do not alter global searchpath from archives.

pull/2/head
Alibek Omarov 2 years ago
parent
commit
0d6137ee40
  1. 14
      filesystem/VFileSystem009.cpp
  2. 19
      filesystem/dir.c
  3. 130
      filesystem/filesystem.c
  4. 20
      filesystem/filesystem_internal.h
  5. 77
      filesystem/pak.c
  6. 67
      filesystem/wad.c
  7. 78
      filesystem/zip.c

14
filesystem/VFileSystem009.cpp

@ -470,11 +470,19 @@ public:
bool AddPackFile( const char *path, const char *id ) override bool AddPackFile( const char *path, const char *id ) override
{ {
char dir[MAX_VA_STRING], fullpath[MAX_VA_STRING]; char dir[MAX_VA_STRING], fullpath[MAX_VA_STRING];
const char *ext = COM_FileExtension( path );
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", IdToDir( dir, sizeof( dir ), id ), path ); IdToDir( dir, sizeof( dir ), id );
CopyAndFixSlashes( fullpath, path, sizeof( fullpath )); Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", dir, path );
COM_FixSlashes( fullpath );
return !!FS_AddPak_Fullpath( fullpath, nullptr, FS_CUSTOM_PATH ); for( const fs_archive_t *archive = g_archives; archive->ext; archive++ )
{
if( archive->type == SEARCHPATH_PAK && !Q_stricmp( ext, archive->ext ))
return FS_AddArchive_Fullpath( archive, fullpath, FS_CUSTOM_PATH );
}
return false;
} }
FileHandle_t OpenFromCacheForRead( const char *path , const char *mode, const char *id ) override FileHandle_t OpenFromCacheForRead( const char *path , const char *mode, const char *id ) override

19
filesystem/dir.c

@ -480,29 +480,12 @@ void FS_InitDirectorySearchpath( searchpath_t *search, const char *path, int fla
FS_PopulateDirEntries( search->dir, path ); FS_PopulateDirEntries( search->dir, path );
} }
searchpath_t *FS_AddDir_Fullpath( const char *path, qboolean *already_loaded, int flags ) searchpath_t *FS_AddDir_Fullpath( const char *path, int flags )
{ {
searchpath_t *search; searchpath_t *search;
for( search = fs_searchpaths; search; search = search->next )
{
if( search->type == SEARCHPATH_PLAIN && !Q_stricmp( search->filename, path ))
{
if( already_loaded )
*already_loaded = true;
return search;
}
}
if( already_loaded )
*already_loaded = false;
search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t )); search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ));
FS_InitDirectorySearchpath( search, path, flags ); FS_InitDirectorySearchpath( search, path, flags );
search->next = fs_searchpaths;
fs_searchpaths = search;
Con_Printf( "Adding directory: %s\n", path ); Con_Printf( "Adding directory: %s\n", path );
return search; return search;

130
filesystem/filesystem.c

@ -56,6 +56,23 @@ searchpath_t *fs_writepath;
static char fs_basedir[MAX_SYSPATH]; // base game directory static char fs_basedir[MAX_SYSPATH]; // base game directory
static char fs_gamedir[MAX_SYSPATH]; // game current directory static char fs_gamedir[MAX_SYSPATH]; // game current directory
// add archives in specific order PAK -> PK3 -> WAD
// so raw WADs takes precedence over WADs included into PAKs and PK3s
const fs_archive_t g_archives[] =
{
{ "pak", SEARCHPATH_PAK, FS_AddPak_Fullpath, true },
{ "pk3", SEARCHPATH_ZIP, FS_AddZip_Fullpath, true },
{ "wad", SEARCHPATH_WAD, FS_AddWad_Fullpath, false },
{ NULL }, // end marker
};
// special fs_archive_t for plain directories
static const fs_archive_t g_directory_archive =
{ NULL, SEARCHPATH_PLAIN, FS_AddDir_Fullpath, false };
// static const fs_archive_t g_android_archive =
// { NULL, SEARCHPATH_ANDROID, FS_AddAndroid_Fullpath, false, false };
#ifdef XASH_REDUCE_FD #ifdef XASH_REDUCE_FD
static file_t *fs_last_readfile; static file_t *fs_last_readfile;
static zip_t *fs_last_zip; static zip_t *fs_last_zip;
@ -278,23 +295,69 @@ void FS_CreatePath( char *path )
} }
} }
searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const char *file, int flags )
{
searchpath_t *search;
for( search = fs_searchpaths; search; search = search->next )
{
if( search->type == archive->type && !Q_stricmp( search->filename, file ))
return search; // already loaded
}
search = archive->pfnAddArchive_Fullpath( file, flags );
if( !search )
return NULL;
search->next = fs_searchpaths;
fs_searchpaths = search;
// time to add in search list all the wads from this archive
if( archive->load_wads )
{
stringlist_t list;
int i;
stringlistinit( &list );
search->pfnSearch( search, &list, "*.wad", true );
stringlistsort( &list ); // keep always sorted
for( i = 0; i < list.numstrings; i++ )
{
searchpath_t *wad;
char fullpath[MAX_SYSPATH];
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", file, list.strings[i] );
if(( wad = FS_AddWad_Fullpath( fullpath, flags )))
{
wad->next = fs_searchpaths;
fs_searchpaths = wad;
}
}
stringlistfreecontents( &list );
}
return search;
}
/* /*
================ ================
FS_AddArchive_Fullpath FS_AddArchive_Fullpath
================ ================
*/ */
static searchpath_t *FS_AddArchive_Fullpath( const char *file, qboolean *already_loaded, int flags ) static searchpath_t *FS_AddExtras_Fullpath( const char *file, int flags )
{ {
const fs_archive_t *archive;
const char *ext = COM_FileExtension( file ); const char *ext = COM_FileExtension( file );
if( !Q_stricmp( ext, "pk3" )) for( archive = g_archives; archive->ext; archive++ )
return FS_AddZip_Fullpath( file, already_loaded, flags ); {
else if ( !Q_stricmp( ext, "pak" )) if( !Q_stricmp( ext, archive->ext ))
return FS_AddPak_Fullpath( file, already_loaded, flags ); return FS_AddArchive_Fullpath( archive, file, flags );
}
// skip wads, this function only meant to be used for extras
return NULL; return NULL;
} }
@ -308,6 +371,7 @@ then loads and adds pak1.pak pak2.pak ...
*/ */
void FS_AddGameDirectory( const char *dir, uint flags ) void FS_AddGameDirectory( const char *dir, uint flags )
{ {
const fs_archive_t *archive;
stringlist_t list; stringlist_t list;
searchpath_t *search; searchpath_t *search;
char fullpath[MAX_SYSPATH]; char fullpath[MAX_SYSPATH];
@ -317,48 +381,30 @@ void FS_AddGameDirectory( const char *dir, uint flags )
listdirectory( &list, dir ); listdirectory( &list, dir );
stringlistsort( &list ); stringlistsort( &list );
// add archives in specific order PAK -> PK3 -> WAD for( archive = g_archives; archive->ext; archive++ )
// so raw WADs takes precedence over WADs included into PAKs and PK3s
for( i = 0; i < list.numstrings; i++ )
{ {
const char *ext = COM_FileExtension( list.strings[i] ); if( archive->type == SEARCHPATH_WAD ) // HACKHACK: wads need direct paths but only in this function
FS_AllowDirectPaths( true );
if( !Q_stricmp( ext, "pak" )) for( i = 0; i < list.numstrings; i++ )
{ {
Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", dir, list.strings[i] ); const char *ext = COM_FileExtension( list.strings[i] );
FS_AddPak_Fullpath( fullpath, NULL, flags );
}
}
for( i = 0; i < list.numstrings; i++ ) if( Q_stricmp( ext, archive->ext ))
{ continue;
const char *ext = COM_FileExtension( list.strings[i] );
if( !Q_stricmp( ext, "pk3" ))
{
Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", dir, list.strings[i] ); Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", dir, list.strings[i] );
FS_AddZip_Fullpath( fullpath, NULL, flags ); FS_AddArchive_Fullpath( archive, fullpath, flags );
} }
}
for( i = 0; i < list.numstrings; i++ ) FS_AllowDirectPaths( false );
{
const char *ext = COM_FileExtension( list.strings[i] );
if( !Q_stricmp( ext, "wad" ))
{
FS_AllowDirectPaths( true );
Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", dir, list.strings[i] );
FS_AddWad_Fullpath( fullpath, NULL, flags );
FS_AllowDirectPaths( false );
}
} }
stringlistfreecontents( &list ); stringlistfreecontents( &list );
// add the directory to the search path // add the directory to the search path
// (unpacked files have the priority over packed files) // (unpacked files have the priority over packed files)
search = FS_AddDir_Fullpath( dir, NULL, flags ); search = FS_AddArchive_Fullpath( &g_directory_archive, dir, flags );
if( !FBitSet( flags, FS_NOWRITE_PATH )) if( !FBitSet( flags, FS_NOWRITE_PATH ))
fs_writepath = search; fs_writepath = search;
} }
@ -1108,25 +1154,13 @@ void FS_Rescan( void )
FS_ClearSearchPath(); FS_ClearSearchPath();
#if XASH_IOS
{
char buf[MAX_VA_STRING];
Q_snprintf( buf, sizeof( buf ), "%sextras.pak", SDL_GetBasePath() );
FS_AddPak_Fullpath( buf, NULL, extrasFlags );
Q_snprintf( buf, sizeof( buf ), "%sextras_%s.pak", SDL_GetBasePath(), GI->gamefolder );
FS_AddPak_Fullpath( buf, NULL, extrasFlags );
}
#else
str = getenv( "XASH3D_EXTRAS_PAK1" ); str = getenv( "XASH3D_EXTRAS_PAK1" );
if( COM_CheckString( str )) if( COM_CheckString( str ))
FS_AddArchive_Fullpath( str, NULL, extrasFlags ); FS_AddExtras_Fullpath( str, extrasFlags );
str = getenv( "XASH3D_EXTRAS_PAK2" ); str = getenv( "XASH3D_EXTRAS_PAK2" );
if( COM_CheckString( str )) if( COM_CheckString( str ))
FS_AddArchive_Fullpath( str, NULL, extrasFlags ); FS_AddExtras_Fullpath( str, extrasFlags );
#endif
if( Q_stricmp( GI->basedir, GI->gamefolder )) if( Q_stricmp( GI->basedir, GI->gamefolder ))
FS_AddGameHierarchy( GI->basedir, 0 ); FS_AddGameHierarchy( GI->basedir, 0 );

20
filesystem/filesystem_internal.h

@ -93,6 +93,16 @@ typedef struct searchpath_s
byte *(*pfnLoadFile)( struct searchpath_s *search, const char *path, int pack_ind, fs_offset_t *filesize ); byte *(*pfnLoadFile)( struct searchpath_s *search, const char *path, int pack_ind, fs_offset_t *filesize );
} searchpath_t; } searchpath_t;
typedef searchpath_t *(*FS_ADDARCHIVE_FULLPATH)( const char *path, int flags );
typedef struct fs_archive_s
{
const char *ext;
int type;
FS_ADDARCHIVE_FULLPATH pfnAddArchive_Fullpath;
qboolean load_wads; // load wads from this archive
} fs_archive_t;
extern fs_globals_t FI; extern fs_globals_t FI;
extern searchpath_t *fs_searchpaths; extern searchpath_t *fs_searchpaths;
extern searchpath_t *fs_writepath; extern searchpath_t *fs_writepath;
@ -102,6 +112,7 @@ extern qboolean fs_ext_path;
extern char fs_rodir[MAX_SYSPATH]; extern char fs_rodir[MAX_SYSPATH];
extern char fs_rootdir[MAX_SYSPATH]; extern char fs_rootdir[MAX_SYSPATH];
extern fs_api_t g_api; extern fs_api_t g_api;
extern const fs_archive_t g_archives[];
#define GI FI.GameInfo #define GI FI.GameInfo
@ -123,6 +134,7 @@ extern fs_api_t g_api;
// //
qboolean FS_InitStdio( qboolean caseinsensitive, const char *rootdir, const char *basedir, const char *gamedir, const char *rodir ); qboolean FS_InitStdio( qboolean caseinsensitive, const char *rootdir, const char *basedir, const char *gamedir, const char *rodir );
void FS_ShutdownStdio( void ); void FS_ShutdownStdio( void );
searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const char *file, int flags );
// search path utils // search path utils
void FS_Rescan( void ); void FS_Rescan( void );
@ -193,22 +205,22 @@ searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t
// //
// pak.c // pak.c
// //
searchpath_t *FS_AddPak_Fullpath( const char *pakfile, qboolean *already_loaded, int flags ); searchpath_t *FS_AddPak_Fullpath( const char *pakfile, int flags );
// //
// wad.c // wad.c
// //
searchpath_t *FS_AddWad_Fullpath( const char *wadfile, qboolean *already_loaded, int flags ); searchpath_t *FS_AddWad_Fullpath( const char *wadfile, int flags );
// //
// zip.c // zip.c
// //
searchpath_t *FS_AddZip_Fullpath( const char *zipfile, qboolean *already_loaded, int flags ); searchpath_t *FS_AddZip_Fullpath( const char *zipfile, int flags );
// //
// dir.c // dir.c
// //
searchpath_t *FS_AddDir_Fullpath( const char *path, qboolean *already_loaded, int flags ); searchpath_t *FS_AddDir_Fullpath( const char *path, int flags );
qboolean FS_FixFileCase( dir_t *dir, const char *path, char *dst, const size_t len, qboolean createpath ); qboolean FS_FixFileCase( dir_t *dir, const char *path, char *dst, const size_t len, qboolean createpath );
void FS_InitDirectorySearchpath( searchpath_t *search, const char *path, int flags ); void FS_InitDirectorySearchpath( searchpath_t *search, const char *path, int flags );

77
filesystem/pak.c

@ -330,66 +330,35 @@ If keep_plain_dirs is set, the pack will be added AFTER the first sequence of
plain directories. plain directories.
================ ================
*/ */
searchpath_t *FS_AddPak_Fullpath( const char *pakfile, qboolean *already_loaded, int flags ) searchpath_t *FS_AddPak_Fullpath( const char *pakfile, int flags )
{ {
searchpath_t *search; searchpath_t *search;
pack_t *pak = NULL; pack_t *pak;
const char *ext = COM_FileExtension( pakfile ); int i, errorcode = PAK_LOAD_COULDNT_OPEN;
int i, errorcode = PAK_LOAD_COULDNT_OPEN;
for( search = fs_searchpaths; search; search = search->next ) pak = FS_LoadPackPAK( pakfile, &errorcode );
{
if( search->type == SEARCHPATH_PAK && !Q_stricmp( search->filename, pakfile ))
{
if( already_loaded ) *already_loaded = true;
return search; // already loaded
}
}
if( already_loaded )
*already_loaded = false;
if( !Q_stricmp( ext, "pak" ))
pak = FS_LoadPackPAK( pakfile, &errorcode );
if( pak )
{
search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ));
Q_strncpy( search->filename, pakfile, sizeof( search->filename ));
search->pack = pak;
search->type = SEARCHPATH_PAK;
search->next = fs_searchpaths;
search->flags = flags;
search->pfnPrintInfo = FS_PrintInfo_PAK;
search->pfnClose = FS_Close_PAK;
search->pfnOpenFile = FS_OpenFile_PAK;
search->pfnFileTime = FS_FileTime_PAK;
search->pfnFindFile = FS_FindFile_PAK;
search->pfnSearch = FS_Search_PAK;
fs_searchpaths = search;
Con_Reportf( "Adding pakfile: %s (%i files)\n", pakfile, pak->numfiles );
// time to add in search list all the wads that contains in current pakfile (if do)
for( i = 0; i < pak->numfiles; i++ )
{
if( !Q_stricmp( COM_FileExtension( pak->files[i].name ), "wad" ))
{
char fullpath[MAX_SYSPATH];
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", pakfile, pak->files[i].name ); if( !pak )
FS_AddWad_Fullpath( fullpath, NULL, flags );
}
}
return search;
}
else
{ {
if( errorcode != PAK_LOAD_NO_FILES ) if( errorcode != PAK_LOAD_NO_FILES )
Con_Reportf( S_ERROR "FS_AddPak_Fullpath: unable to load pak \"%s\"\n", pakfile ); Con_Reportf( S_ERROR "FS_AddPak_Fullpath: unable to load pak \"%s\"\n", pakfile );
return NULL; return NULL;
} }
search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ));
Q_strncpy( search->filename, pakfile, sizeof( search->filename ));
search->pack = pak;
search->type = SEARCHPATH_PAK;
search->flags = flags;
search->pfnPrintInfo = FS_PrintInfo_PAK;
search->pfnClose = FS_Close_PAK;
search->pfnOpenFile = FS_OpenFile_PAK;
search->pfnFileTime = FS_FileTime_PAK;
search->pfnFindFile = FS_FindFile_PAK;
search->pfnSearch = FS_Search_PAK;
Con_Reportf( "Adding pakfile: %s (%i files)\n", pakfile, pak->numfiles );
return search;
} }

67
filesystem/wad.c

@ -635,52 +635,35 @@ static byte *W_ReadLump( searchpath_t *search, const char *path, int pack_ind, f
FS_AddWad_Fullpath FS_AddWad_Fullpath
==================== ====================
*/ */
searchpath_t *FS_AddWad_Fullpath( const char *wadfile, qboolean *already_loaded, int flags ) searchpath_t *FS_AddWad_Fullpath( const char *wadfile, int flags )
{ {
searchpath_t *search; searchpath_t *search;
wfile_t *wad = NULL; wfile_t *wad;
const char *ext = COM_FileExtension( wadfile ); int errorcode = WAD_LOAD_COULDNT_OPEN;
int errorcode = WAD_LOAD_COULDNT_OPEN;
for( search = fs_searchpaths; search; search = search->next ) wad = W_Open( wadfile, &errorcode );
{
if( search->type == SEARCHPATH_WAD && !Q_stricmp( search->filename, wadfile ))
{
if( already_loaded ) *already_loaded = true;
return search; // already loaded
}
}
if( already_loaded ) if( !wad )
*already_loaded = false;
if( !Q_stricmp( ext, "wad" ))
wad = W_Open( wadfile, &errorcode );
if( wad )
{ {
search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t )); if( errorcode != WAD_LOAD_NO_FILES )
Q_strncpy( search->filename, wadfile, sizeof( search->filename )); Con_Reportf( S_ERROR "FS_AddWad_Fullpath: unable to load wad \"%s\"\n", wadfile );
search->wad = wad; return NULL;
search->type = SEARCHPATH_WAD;
search->next = fs_searchpaths;
search->flags = flags;
search->pfnPrintInfo = FS_PrintInfo_WAD;
search->pfnClose = FS_Close_WAD;
search->pfnOpenFile = FS_OpenFile_WAD;
search->pfnFileTime = FS_FileTime_WAD;
search->pfnFindFile = FS_FindFile_WAD;
search->pfnSearch = FS_Search_WAD;
search->pfnLoadFile = W_ReadLump;
fs_searchpaths = search;
Con_Reportf( "Adding wadfile: %s (%i files)\n", wadfile, wad->numlumps );
return search;
} }
if( errorcode != WAD_LOAD_NO_FILES ) search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ));
Con_Reportf( S_ERROR "FS_AddWad_Fullpath: unable to load wad \"%s\"\n", wadfile ); Q_strncpy( search->filename, wadfile, sizeof( search->filename ));
return NULL; search->wad = wad;
search->type = SEARCHPATH_WAD;
search->flags = flags;
search->pfnPrintInfo = FS_PrintInfo_WAD;
search->pfnClose = FS_Close_WAD;
search->pfnOpenFile = FS_OpenFile_WAD;
search->pfnFileTime = FS_FileTime_WAD;
search->pfnFindFile = FS_FindFile_WAD;
search->pfnSearch = FS_Search_WAD;
search->pfnLoadFile = W_ReadLump;
Con_Reportf( "Adding wadfile: %s (%i files)\n", wadfile, wad->numlumps );
return search;
} }

78
filesystem/zip.c

@ -664,68 +664,36 @@ FS_AddZip_Fullpath
=========== ===========
*/ */
searchpath_t *FS_AddZip_Fullpath( const char *zipfile, qboolean *already_loaded, int flags ) searchpath_t *FS_AddZip_Fullpath( const char *zipfile, int flags )
{ {
searchpath_t *search; searchpath_t *search;
zip_t *zip = NULL; zip_t *zip;
const char *ext = COM_FileExtension( zipfile ); int i, errorcode = ZIP_LOAD_COULDNT_OPEN;
int errorcode = ZIP_LOAD_COULDNT_OPEN;
for( search = fs_searchpaths; search; search = search->next ) zip = FS_LoadZip( zipfile, &errorcode );
{
if( search->type == SEARCHPATH_ZIP && !Q_stricmp( search->filename, zipfile ))
{
if( already_loaded ) *already_loaded = true;
return search; // already loaded
}
}
if( already_loaded ) *already_loaded = false; if( !zip )
if( !Q_stricmp( ext, "pk3" ) )
zip = FS_LoadZip( zipfile, &errorcode );
if( zip )
{
string fullpath;
int i;
search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ) );
Q_strncpy( search->filename, zipfile, sizeof( search->filename ));
search->zip = zip;
search->type = SEARCHPATH_ZIP;
search->next = fs_searchpaths;
search->flags = flags;
search->pfnPrintInfo = FS_PrintInfo_ZIP;
search->pfnClose = FS_Close_ZIP;
search->pfnOpenFile = FS_OpenFile_ZIP;
search->pfnFileTime = FS_FileTime_ZIP;
search->pfnFindFile = FS_FindFile_ZIP;
search->pfnSearch = FS_Search_ZIP;
search->pfnLoadFile = FS_LoadZIPFile;
fs_searchpaths = search;
Con_Reportf( "Adding zipfile: %s (%i files)\n", zipfile, zip->numfiles );
// time to add in search list all the wads that contains in current pakfile (if do)
for( i = 0; i < zip->numfiles; i++ )
{
if( !Q_stricmp( COM_FileExtension( zip->files[i].name ), "wad" ))
{
Q_snprintf( fullpath, MAX_STRING, "%s/%s", zipfile, zip->files[i].name );
FS_AddWad_Fullpath( fullpath, NULL, flags );
}
}
return search;
}
else
{ {
if( errorcode != ZIP_LOAD_NO_FILES ) if( errorcode != ZIP_LOAD_NO_FILES )
Con_Reportf( S_ERROR "FS_AddZip_Fullpath: unable to load zip \"%s\"\n", zipfile ); Con_Reportf( S_ERROR "FS_AddZip_Fullpath: unable to load zip \"%s\"\n", zipfile );
return NULL; return NULL;
} }
search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ) );
Q_strncpy( search->filename, zipfile, sizeof( search->filename ));
search->zip = zip;
search->type = SEARCHPATH_ZIP;
search->flags = flags;
search->pfnPrintInfo = FS_PrintInfo_ZIP;
search->pfnClose = FS_Close_ZIP;
search->pfnOpenFile = FS_OpenFile_ZIP;
search->pfnFileTime = FS_FileTime_ZIP;
search->pfnFindFile = FS_FindFile_ZIP;
search->pfnSearch = FS_Search_ZIP;
search->pfnLoadFile = FS_LoadZIPFile;
Con_Reportf( "Adding zipfile: %s (%i files)\n", zipfile, zip->numfiles );
return search;
} }

Loading…
Cancel
Save