1
0
Fork 0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-11-04 18:29:41 +01:00
VVVVVV/desktop_version/src/FileSystemUtils.cpp

1311 lines
30 KiB
C++
Raw Normal View History

#include "FileSystemUtils.h"
#include <physfs.h>
#include <SDL.h>
#include <stdarg.h>
2020-01-01 21:29:24 +01:00
#include <stdio.h>
#include <tinyxml2.h>
2020-01-01 21:29:24 +01:00
#include "Alloc.h"
#include "BinaryBlob.h"
#include "Constants.h"
#include "Exit.h"
#include "Graphics.h"
#include "Localization.h"
Switch assets mounting to dedicated directory This fixes an issue where you would be able to mount things other than custom assets in per-level custom asset directories and zips. To be fair, the effects of this issue were fairly limited - about the only thing I could do with it was to override a user-made quicksave of a custom level with one of my own. However, since the quicksave check happens before assets are mounted, if the user didn't have an existing quicksave then they wouldn't be able load my quicksave. Furthermore, mounting things like settings.vvv simply doesn't work because assets only get mounted when the level gets loaded, but the game only reads from settings.vvv on startup. Still, this is an issue, and just because it only has one effect doesn't mean we should single-case patch that one effect only. So what can we do? I was thinking that we should (1) mount custom assets in a dedicated directory, and then from there (2) mount each specific asset directly - namely, mount the graphics/ and sounds/ folders, and mount the vvvvvvmusic.vvv and mmmmmm.vvv files. For (1), assets are now mounted at a (non-existent) location named .vvv-mnt/assets/. However, (2) doesn't fully work due to how PhysFS works. What DOES work is being able to mount the graphics/ and sounds/ folders, but only if the custom assets directory is a directory. And, you actually have to use the real directory where those graphics/ and sounds/ folders are located, and not the mounted directory, because PHYSFS_mount() only accepts real directories. (In which case why bother mounting the directory in the first place if we have to use real directories anyway?) So already this seems like having different directory and zip mounting paths, which I don't want... I tried to unify the directory and zip paths and get around the real directory limitation. So for mounting each individual asset (i.e. graphics/, sounds/, but especially vvvvvvmusic.vvv and mmmmmm.vvv), I tried doing PHYSFS_openRead() followed by PHYSFS_mountHandle() with that PHYSFS_File, but this simply doesn't work, because PHYSFS_mountHandle() will always create a PHYSFS_Io object, and pass it to a PhysFS internal helper function named openDirectory() which will only attempt to treat it as a directory if the PHYSFS_Io* passed is NULL. Since PHYSFS_mountHandle() always passes a non-NULL PHYSFS_Io*, openDirectory() will always treat it like a zip file and never as a directory - in contrast, PHYSFS_mount() will always pass a NULL PHYSFS_Io* to openDirectory(), so PHYSFS_mount() is the only function that works for mounting directories. (And even if this did work, having to keep the file open (because of the PHYSFS_openRead()) results in the user being unable to touch the file on Windows until it gets closed, which I also don't want.) As for zip files, PHYSFS_mount() works just fine on them, but then we run into the issue of accessing the individual assets inside it. As covered above, PHYSFS_mount() only accepts real directories, so we can't use it to access the assets inside, but then if we do the PHYSFS_openRead() and PHYSFS_mountHandle() approach, PHYSFS_mountHandle() will treat the assets inside as zip files instead of just mounting them normally! So in short, PhysFS only seems to be able to mount directories and zip files, and not any loose individual files (like vvvvvvmusic.vvv and mmmmmm.vvv). Furthermore, directories inside directories works, but directories inside zip files doesn't (only zip files inside zip files work). It seems like our asset paths don't really work well with PhysFS's design. Currently, graphics/, sounds/, vvvvvvmusic.vvv, and mmmmmm.vvv all live at the root directory of the VVVVVV folder. But what would work better is if all of those items were organized into a subfolder, for example, a folder named assets/. So the previous assets mounting system before this patch would just have mounted assets/ and be done with it, and there would be no risk of mounting extraneous files that could do bad things. However, due to our unorganized asset paths, the previous system has to mount assets at the root of the VVVVVV folder, which invites the possibility of those extraneous bad files being mounted. Well, we can't change the asset paths now, that would be a pretty big API break (maybe it should be a 2.4 thing). So what can we do? What I've done is, after mounting the assets at .vvv-mnt/assets/, when the game loads an asset, it checks if there's an override available inside .vvv-mnt/assets/, and if so, the game will load that asset instead of the regular one. This is basically reimplementing what PhysFS SHOULD be able to do for us, but can't. This fixes the issue of being able to mount a quicksave for a custom level inside its asset directory. I should also note, the unorganized asset paths issue also means that for .zip files (which contain the level file), the level file itself is also technically mounted at .vvv-mnt/assets/. This is harmless (because when we load a level file, we never load it as an asset) but it's still a bit ugly. Changing the asset paths now seems more and more like a good thing to do...
2021-04-05 20:32:10 +02:00
#include "Maths.h"
#include "Screen.h"
#include "Unused.h"
#include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h"
2020-06-12 22:20:18 +02:00
/* These are needed for PLATFORM_* crap */
2020-01-01 21:29:24 +01:00
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
2020-01-01 21:29:24 +01:00
#include <windows.h>
#include <shlobj.h>
static int mkdir(char* path, int mode)
{
WCHAR utf16_path[MAX_PATH];
MultiByteToWideChar(CP_UTF8, 0, path, -1, utf16_path, MAX_PATH);
return CreateDirectoryW(utf16_path, NULL);
}
#elif defined(__EMSCRIPTEN__)
#include <limits.h>
#include <sys/stat.h>
#include <emscripten.h>
#define MAX_PATH PATH_MAX
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__DragonFly__) || defined(__unix__)
2020-06-12 22:20:18 +02:00
#include <limits.h>
#include <sys/stat.h>
2020-01-01 21:29:24 +01:00
#define MAX_PATH PATH_MAX
#endif
static bool isInit = false;
static const char* pathSep = NULL;
static char* basePath = NULL;
static char writeDir[MAX_PATH] = {'\0'};
static char saveDir[MAX_PATH] = {'\0'};
static char levelDir[MAX_PATH] = {'\0'};
static char mainLangDir[MAX_PATH] = {'\0'};
static bool isMainLangDirFromRepo = false;
static bool doesLangDirExist = false;
static bool doesFontsDirExist = false;
2020-01-01 21:29:24 +01:00
static char assetDir[MAX_PATH] = {'\0'};
Switch assets mounting to dedicated directory This fixes an issue where you would be able to mount things other than custom assets in per-level custom asset directories and zips. To be fair, the effects of this issue were fairly limited - about the only thing I could do with it was to override a user-made quicksave of a custom level with one of my own. However, since the quicksave check happens before assets are mounted, if the user didn't have an existing quicksave then they wouldn't be able load my quicksave. Furthermore, mounting things like settings.vvv simply doesn't work because assets only get mounted when the level gets loaded, but the game only reads from settings.vvv on startup. Still, this is an issue, and just because it only has one effect doesn't mean we should single-case patch that one effect only. So what can we do? I was thinking that we should (1) mount custom assets in a dedicated directory, and then from there (2) mount each specific asset directly - namely, mount the graphics/ and sounds/ folders, and mount the vvvvvvmusic.vvv and mmmmmm.vvv files. For (1), assets are now mounted at a (non-existent) location named .vvv-mnt/assets/. However, (2) doesn't fully work due to how PhysFS works. What DOES work is being able to mount the graphics/ and sounds/ folders, but only if the custom assets directory is a directory. And, you actually have to use the real directory where those graphics/ and sounds/ folders are located, and not the mounted directory, because PHYSFS_mount() only accepts real directories. (In which case why bother mounting the directory in the first place if we have to use real directories anyway?) So already this seems like having different directory and zip mounting paths, which I don't want... I tried to unify the directory and zip paths and get around the real directory limitation. So for mounting each individual asset (i.e. graphics/, sounds/, but especially vvvvvvmusic.vvv and mmmmmm.vvv), I tried doing PHYSFS_openRead() followed by PHYSFS_mountHandle() with that PHYSFS_File, but this simply doesn't work, because PHYSFS_mountHandle() will always create a PHYSFS_Io object, and pass it to a PhysFS internal helper function named openDirectory() which will only attempt to treat it as a directory if the PHYSFS_Io* passed is NULL. Since PHYSFS_mountHandle() always passes a non-NULL PHYSFS_Io*, openDirectory() will always treat it like a zip file and never as a directory - in contrast, PHYSFS_mount() will always pass a NULL PHYSFS_Io* to openDirectory(), so PHYSFS_mount() is the only function that works for mounting directories. (And even if this did work, having to keep the file open (because of the PHYSFS_openRead()) results in the user being unable to touch the file on Windows until it gets closed, which I also don't want.) As for zip files, PHYSFS_mount() works just fine on them, but then we run into the issue of accessing the individual assets inside it. As covered above, PHYSFS_mount() only accepts real directories, so we can't use it to access the assets inside, but then if we do the PHYSFS_openRead() and PHYSFS_mountHandle() approach, PHYSFS_mountHandle() will treat the assets inside as zip files instead of just mounting them normally! So in short, PhysFS only seems to be able to mount directories and zip files, and not any loose individual files (like vvvvvvmusic.vvv and mmmmmm.vvv). Furthermore, directories inside directories works, but directories inside zip files doesn't (only zip files inside zip files work). It seems like our asset paths don't really work well with PhysFS's design. Currently, graphics/, sounds/, vvvvvvmusic.vvv, and mmmmmm.vvv all live at the root directory of the VVVVVV folder. But what would work better is if all of those items were organized into a subfolder, for example, a folder named assets/. So the previous assets mounting system before this patch would just have mounted assets/ and be done with it, and there would be no risk of mounting extraneous files that could do bad things. However, due to our unorganized asset paths, the previous system has to mount assets at the root of the VVVVVV folder, which invites the possibility of those extraneous bad files being mounted. Well, we can't change the asset paths now, that would be a pretty big API break (maybe it should be a 2.4 thing). So what can we do? What I've done is, after mounting the assets at .vvv-mnt/assets/, when the game loads an asset, it checks if there's an override available inside .vvv-mnt/assets/, and if so, the game will load that asset instead of the regular one. This is basically reimplementing what PhysFS SHOULD be able to do for us, but can't. This fixes the issue of being able to mount a quicksave for a custom level inside its asset directory. I should also note, the unorganized asset paths issue also means that for .zip files (which contain the level file), the level file itself is also technically mounted at .vvv-mnt/assets/. This is harmless (because when we load a level file, we never load it as an asset) but it's still a bit ugly. Changing the asset paths now seems more and more like a good thing to do...
2021-04-05 20:32:10 +02:00
static char virtualMountPath[MAX_PATH] = {'\0'};
static int PLATFORM_getOSDirectory(char* output, const size_t output_size);
2020-01-01 21:29:24 +01:00
static void* bridged_malloc(PHYSFS_uint64 size)
{
return SDL_malloc(size);
}
static void* bridged_realloc(void* ptr, PHYSFS_uint64 size)
{
return SDL_realloc(ptr, size);
}
static const PHYSFS_Allocator allocator = {
NULL,
NULL,
bridged_malloc,
bridged_realloc,
SDL_free
};
static bool mount_pre_datazip(
char* out_path,
const char* real_dirname,
const char* mount_point,
const char* user_path
)
{
/* Find and mount a directory (like the main language directory) in front of data.zip.
* This directory, if not user-supplied, can be either next to data.zip,
* or otherwise in desktop_version/ if that's found in the base path.
*
* out_path is assumed to be either NULL, or MAX_PATH long. If it isn't, boom */
if (user_path != NULL)
{
if (PHYSFS_mount(user_path, mount_point, 1))
{
if (out_path != NULL)
{
SDL_strlcpy(out_path, user_path, MAX_PATH);
}
return true;
}
vlog_warn("User-supplied %s directory is invalid!", real_dirname);
return false;
}
/* Try to detect the directory, it's next to data.zip in distributed builds */
bool dir_found = false;
char buffer[MAX_PATH];
SDL_snprintf(buffer, sizeof(buffer), "%s%s%s",
basePath,
real_dirname,
pathSep
);
if (PHYSFS_mount(buffer, mount_point, 1))
{
dir_found = true;
}
else
{
/* If you're a developer, you probably want to use the language files/fonts
* from the repo, otherwise it's a pain to keep everything in sync.
* And who knows how deep in build folders our binary is. */
size_t buf_reserve = SDL_strlen(real_dirname)+1;
SDL_strlcpy(buffer, basePath, sizeof(buffer)-buf_reserve);
char needle[32];
SDL_snprintf(needle, sizeof(needle), "%sdesktop_version%s",
pathSep,
pathSep
);
/* We want the last match */
char* match_last = NULL;
char* match = buffer;
while ((match = SDL_strstr(match, needle)))
{
match_last = match;
match = &match[1];
}
if (match_last != NULL)
{
/* strstr only gives us a pointer and not a remaining buffer length, but that's
* why we pretended the buffer was `buf_reserve` chars shorter than it was! */
SDL_strlcpy(&match_last[SDL_strlen(needle)], real_dirname, buf_reserve);
SDL_strlcat(buffer, pathSep, sizeof(buffer));
if (PHYSFS_mount(buffer, mount_point, 1))
{
dir_found = true;
if (SDL_strcmp(real_dirname, "lang") == 0)
{
loc::show_translator_menu = true;
isMainLangDirFromRepo = true;
}
}
}
}
if (dir_found)
{
if (out_path != NULL)
{
SDL_strlcpy(out_path, buffer, MAX_PATH);
}
}
else
{
vlog_warn("Cannot find the %s directory anywhere!", real_dirname);
}
return dir_found;
}
int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath, char* langDir, char* fontsDir)
2020-01-01 21:29:24 +01:00
{
char output[MAX_PATH];
pathSep = PHYSFS_getDirSeparator();
PHYSFS_setAllocator(&allocator);
if (!PHYSFS_init(argvZero))
{
vlog_error(
"Unable to initialize PhysFS: %s",
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return 0;
}
PHYSFS_permitSymbolicLinks(1);
/* Determine the OS user directory */
if (baseDir && baseDir[0] != '\0')
{
/* We later append to this path and assume it ends in a slash */
bool trailing_pathsep = SDL_strcmp(baseDir + SDL_strlen(baseDir) - SDL_strlen(pathSep), pathSep) == 0;
SDL_snprintf(output, sizeof(output), "%s%s",
baseDir,
!trailing_pathsep ? pathSep : ""
);
}
else if (!PLATFORM_getOSDirectory(output, sizeof(output)))
{
return 0;
}
/* Mount our base user directory */
SDL_strlcpy(writeDir, output, sizeof(writeDir));
if (!PHYSFS_mount(writeDir, NULL, 0))
{
vlog_error(
"Could not mount %s: %s",
writeDir,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return 0;
}
if (!PHYSFS_setWriteDir(writeDir))
{
vlog_error(
"Could not set write dir to %s: %s",
writeDir,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return 0;
}
vlog_info("Base directory: %s", writeDir);
/* Store full save directory */
SDL_snprintf(saveDir, sizeof(saveDir), "%s%s%s",
writeDir,
"saves",
pathSep
);
mkdir(saveDir, 0777);
vlog_info("Save directory: %s", saveDir);
/* Store full level directory */
SDL_snprintf(levelDir, sizeof(levelDir), "%s%s%s",
writeDir,
"levels",
pathSep
);
mkdir(levelDir, 0777);
vlog_info("Level directory: %s", levelDir);
basePath = SDL_GetBasePath();
if (basePath == NULL)
{
vlog_warn("Unable to determine base path, falling back to current directory");
basePath = SDL_strdup("./");
}
doesLangDirExist = mount_pre_datazip(mainLangDir, "lang", "lang/", langDir);
vlog_info("Languages directory: %s", mainLangDir);
doesFontsDirExist = mount_pre_datazip(NULL, "fonts", "graphics/", fontsDir);
/* Mount the stock content last */
if (assetsPath)
{
SDL_strlcpy(output, assetsPath, sizeof(output));
}
else
{
SDL_snprintf(output, sizeof(output), "%s%s",
basePath,
"data.zip"
);
}
if (!PHYSFS_mount(output, NULL, 1))
{
vlog_error("Error: data.zip missing!");
vlog_error("You do not have data.zip!");
vlog_error("Grab it from your purchased copy of the game,");
vlog_error("or get it from the free Make and Play Edition.");
SDL_ShowSimpleMessageBox(
SDL_MESSAGEBOX_ERROR,
"data.zip missing!",
"You do not have data.zip!"
"\n\nGrab it from your purchased copy of the game,"
"\nor get it from the free Make and Play Edition.",
NULL
);
return 0;
}
SDL_snprintf(output, sizeof(output), "%s%s", basePath, "gamecontrollerdb.txt");
if (SDL_GameControllerAddMappingsFromFile(output) < 0)
{
vlog_info("gamecontrollerdb.txt not found!");
}
isInit = true;
return 1;
2020-01-01 21:29:24 +01:00
}
bool FILESYSTEM_isInit(void)
{
return isInit;
}
static unsigned char* stdin_buffer = NULL;
static size_t stdin_length = 0;
void FILESYSTEM_deinit(void)
2020-01-01 21:29:24 +01:00
{
if (PHYSFS_isInit())
{
PHYSFS_deinit();
}
VVV_free(stdin_buffer);
VVV_free(basePath);
isInit = false;
2020-01-01 21:29:24 +01:00
}
char *FILESYSTEM_getUserSaveDirectory(void)
2020-01-01 21:29:24 +01:00
{
return saveDir;
2020-01-01 21:29:24 +01:00
}
char *FILESYSTEM_getUserLevelDirectory(void)
2020-01-01 21:29:24 +01:00
{
return levelDir;
2020-01-01 21:29:24 +01:00
}
char *FILESYSTEM_getUserMainLangDirectory(void)
{
return mainLangDir;
}
bool FILESYSTEM_isMainLangDirFromRepo(void)
{
return isMainLangDirFromRepo;
}
bool FILESYSTEM_doesLangDirExist(void)
{
return doesLangDirExist;
}
bool FILESYSTEM_doesFontsDirExist(void)
{
return doesFontsDirExist;
}
bool FILESYSTEM_restoreWriteDir(void)
{
return PHYSFS_setWriteDir(writeDir);
}
bool FILESYSTEM_setLangWriteDir(void)
{
const char* realLangDir = PHYSFS_getRealDir("lang");
if (realLangDir == NULL || SDL_strcmp(mainLangDir, realLangDir) != 0)
{
vlog_error("Not setting language write dir: %s overrules %s when loading",
realLangDir, mainLangDir
);
return false;
}
if (!PHYSFS_setWriteDir(mainLangDir))
{
FILESYSTEM_restoreWriteDir();
return false;
}
return true;
}
bool FILESYSTEM_isFileType(const char* filename, PHYSFS_FileType filetype)
{
PHYSFS_Stat stat;
bool success = PHYSFS_stat(filename, &stat);
if (!success)
{
vlog_error(
"Could not stat file: %s",
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return false;
}
/* We unfortunately cannot follow symlinks (PhysFS limitation).
* Let the caller deal with them.
*/
return stat.filetype == filetype
|| stat.filetype == PHYSFS_FILETYPE_SYMLINK;
}
bool FILESYSTEM_isFile(const char* filename)
{
return FILESYSTEM_isFileType(filename, PHYSFS_FILETYPE_REGULAR);
}
bool FILESYSTEM_isDirectory(const char* filename)
{
return FILESYSTEM_isFileType(filename, PHYSFS_FILETYPE_DIRECTORY);
}
bool FILESYSTEM_isMounted(const char* filename)
{
return PHYSFS_getMountPoint(filename) != NULL;
}
static bool FILESYSTEM_exists(const char *fname)
{
return PHYSFS_exists(fname);
}
static void generateBase36(char* string, const size_t string_size)
Switch assets mounting to dedicated directory This fixes an issue where you would be able to mount things other than custom assets in per-level custom asset directories and zips. To be fair, the effects of this issue were fairly limited - about the only thing I could do with it was to override a user-made quicksave of a custom level with one of my own. However, since the quicksave check happens before assets are mounted, if the user didn't have an existing quicksave then they wouldn't be able load my quicksave. Furthermore, mounting things like settings.vvv simply doesn't work because assets only get mounted when the level gets loaded, but the game only reads from settings.vvv on startup. Still, this is an issue, and just because it only has one effect doesn't mean we should single-case patch that one effect only. So what can we do? I was thinking that we should (1) mount custom assets in a dedicated directory, and then from there (2) mount each specific asset directly - namely, mount the graphics/ and sounds/ folders, and mount the vvvvvvmusic.vvv and mmmmmm.vvv files. For (1), assets are now mounted at a (non-existent) location named .vvv-mnt/assets/. However, (2) doesn't fully work due to how PhysFS works. What DOES work is being able to mount the graphics/ and sounds/ folders, but only if the custom assets directory is a directory. And, you actually have to use the real directory where those graphics/ and sounds/ folders are located, and not the mounted directory, because PHYSFS_mount() only accepts real directories. (In which case why bother mounting the directory in the first place if we have to use real directories anyway?) So already this seems like having different directory and zip mounting paths, which I don't want... I tried to unify the directory and zip paths and get around the real directory limitation. So for mounting each individual asset (i.e. graphics/, sounds/, but especially vvvvvvmusic.vvv and mmmmmm.vvv), I tried doing PHYSFS_openRead() followed by PHYSFS_mountHandle() with that PHYSFS_File, but this simply doesn't work, because PHYSFS_mountHandle() will always create a PHYSFS_Io object, and pass it to a PhysFS internal helper function named openDirectory() which will only attempt to treat it as a directory if the PHYSFS_Io* passed is NULL. Since PHYSFS_mountHandle() always passes a non-NULL PHYSFS_Io*, openDirectory() will always treat it like a zip file and never as a directory - in contrast, PHYSFS_mount() will always pass a NULL PHYSFS_Io* to openDirectory(), so PHYSFS_mount() is the only function that works for mounting directories. (And even if this did work, having to keep the file open (because of the PHYSFS_openRead()) results in the user being unable to touch the file on Windows until it gets closed, which I also don't want.) As for zip files, PHYSFS_mount() works just fine on them, but then we run into the issue of accessing the individual assets inside it. As covered above, PHYSFS_mount() only accepts real directories, so we can't use it to access the assets inside, but then if we do the PHYSFS_openRead() and PHYSFS_mountHandle() approach, PHYSFS_mountHandle() will treat the assets inside as zip files instead of just mounting them normally! So in short, PhysFS only seems to be able to mount directories and zip files, and not any loose individual files (like vvvvvvmusic.vvv and mmmmmm.vvv). Furthermore, directories inside directories works, but directories inside zip files doesn't (only zip files inside zip files work). It seems like our asset paths don't really work well with PhysFS's design. Currently, graphics/, sounds/, vvvvvvmusic.vvv, and mmmmmm.vvv all live at the root directory of the VVVVVV folder. But what would work better is if all of those items were organized into a subfolder, for example, a folder named assets/. So the previous assets mounting system before this patch would just have mounted assets/ and be done with it, and there would be no risk of mounting extraneous files that could do bad things. However, due to our unorganized asset paths, the previous system has to mount assets at the root of the VVVVVV folder, which invites the possibility of those extraneous bad files being mounted. Well, we can't change the asset paths now, that would be a pretty big API break (maybe it should be a 2.4 thing). So what can we do? What I've done is, after mounting the assets at .vvv-mnt/assets/, when the game loads an asset, it checks if there's an override available inside .vvv-mnt/assets/, and if so, the game will load that asset instead of the regular one. This is basically reimplementing what PhysFS SHOULD be able to do for us, but can't. This fixes the issue of being able to mount a quicksave for a custom level inside its asset directory. I should also note, the unorganized asset paths issue also means that for .zip files (which contain the level file), the level file itself is also technically mounted at .vvv-mnt/assets/. This is harmless (because when we load a level file, we never load it as an asset) but it's still a bit ugly. Changing the asset paths now seems more and more like a good thing to do...
2021-04-05 20:32:10 +02:00
{
size_t i;
for (i = 0; i < string_size - 1; ++i)
{
/* a-z0-9 */
char randchar = fRandom() * 35;
if (randchar < 26)
{
randchar += 'a';
}
else
{
randchar -= 26;
randchar += '0';
}
string[i] = randchar;
}
string[string_size - 1] = '\0';
}
static void generateVirtualMountPath(char* path, const size_t path_size)
{
char random_str[6 + 1];
generateBase36(random_str, sizeof(random_str));
SDL_snprintf(
path,
path_size,
".vvv-mnt-virtual-%s/custom-assets/",
random_str
);
Switch assets mounting to dedicated directory This fixes an issue where you would be able to mount things other than custom assets in per-level custom asset directories and zips. To be fair, the effects of this issue were fairly limited - about the only thing I could do with it was to override a user-made quicksave of a custom level with one of my own. However, since the quicksave check happens before assets are mounted, if the user didn't have an existing quicksave then they wouldn't be able load my quicksave. Furthermore, mounting things like settings.vvv simply doesn't work because assets only get mounted when the level gets loaded, but the game only reads from settings.vvv on startup. Still, this is an issue, and just because it only has one effect doesn't mean we should single-case patch that one effect only. So what can we do? I was thinking that we should (1) mount custom assets in a dedicated directory, and then from there (2) mount each specific asset directly - namely, mount the graphics/ and sounds/ folders, and mount the vvvvvvmusic.vvv and mmmmmm.vvv files. For (1), assets are now mounted at a (non-existent) location named .vvv-mnt/assets/. However, (2) doesn't fully work due to how PhysFS works. What DOES work is being able to mount the graphics/ and sounds/ folders, but only if the custom assets directory is a directory. And, you actually have to use the real directory where those graphics/ and sounds/ folders are located, and not the mounted directory, because PHYSFS_mount() only accepts real directories. (In which case why bother mounting the directory in the first place if we have to use real directories anyway?) So already this seems like having different directory and zip mounting paths, which I don't want... I tried to unify the directory and zip paths and get around the real directory limitation. So for mounting each individual asset (i.e. graphics/, sounds/, but especially vvvvvvmusic.vvv and mmmmmm.vvv), I tried doing PHYSFS_openRead() followed by PHYSFS_mountHandle() with that PHYSFS_File, but this simply doesn't work, because PHYSFS_mountHandle() will always create a PHYSFS_Io object, and pass it to a PhysFS internal helper function named openDirectory() which will only attempt to treat it as a directory if the PHYSFS_Io* passed is NULL. Since PHYSFS_mountHandle() always passes a non-NULL PHYSFS_Io*, openDirectory() will always treat it like a zip file and never as a directory - in contrast, PHYSFS_mount() will always pass a NULL PHYSFS_Io* to openDirectory(), so PHYSFS_mount() is the only function that works for mounting directories. (And even if this did work, having to keep the file open (because of the PHYSFS_openRead()) results in the user being unable to touch the file on Windows until it gets closed, which I also don't want.) As for zip files, PHYSFS_mount() works just fine on them, but then we run into the issue of accessing the individual assets inside it. As covered above, PHYSFS_mount() only accepts real directories, so we can't use it to access the assets inside, but then if we do the PHYSFS_openRead() and PHYSFS_mountHandle() approach, PHYSFS_mountHandle() will treat the assets inside as zip files instead of just mounting them normally! So in short, PhysFS only seems to be able to mount directories and zip files, and not any loose individual files (like vvvvvvmusic.vvv and mmmmmm.vvv). Furthermore, directories inside directories works, but directories inside zip files doesn't (only zip files inside zip files work). It seems like our asset paths don't really work well with PhysFS's design. Currently, graphics/, sounds/, vvvvvvmusic.vvv, and mmmmmm.vvv all live at the root directory of the VVVVVV folder. But what would work better is if all of those items were organized into a subfolder, for example, a folder named assets/. So the previous assets mounting system before this patch would just have mounted assets/ and be done with it, and there would be no risk of mounting extraneous files that could do bad things. However, due to our unorganized asset paths, the previous system has to mount assets at the root of the VVVVVV folder, which invites the possibility of those extraneous bad files being mounted. Well, we can't change the asset paths now, that would be a pretty big API break (maybe it should be a 2.4 thing). So what can we do? What I've done is, after mounting the assets at .vvv-mnt/assets/, when the game loads an asset, it checks if there's an override available inside .vvv-mnt/assets/, and if so, the game will load that asset instead of the regular one. This is basically reimplementing what PhysFS SHOULD be able to do for us, but can't. This fixes the issue of being able to mount a quicksave for a custom level inside its asset directory. I should also note, the unorganized asset paths issue also means that for .zip files (which contain the level file), the level file itself is also technically mounted at .vvv-mnt/assets/. This is harmless (because when we load a level file, we never load it as an asset) but it's still a bit ugly. Changing the asset paths now seems more and more like a good thing to do...
2021-04-05 20:32:10 +02:00
}
static char levelDirError[6*SCREEN_WIDTH_CHARS + 1] = {'\0'};
static bool levelDirHasError = false;
bool FILESYSTEM_levelDirHasError(void)
{
return levelDirHasError;
}
void FILESYSTEM_clearLevelDirError(void)
{
levelDirHasError = false;
}
const char* FILESYSTEM_getLevelDirError(void)
{
return levelDirError;
}
void FILESYSTEM_setLevelDirError(const char* text, const char* args_index, ...)
{
levelDirHasError = true;
va_list list;
va_start(list, args_index);
vformat_buf_valist(levelDirError, sizeof(levelDirError), text, args_index, list);
va_end(list);
vlog_error("%s", levelDirError);
}
static bool FILESYSTEM_mountAssetsFrom(const char *fname)
{
const char* real_dir = PHYSFS_getRealDir(fname);
char path[MAX_PATH];
if (real_dir == NULL)
{
FILESYSTEM_setLevelDirError(
loc::gettext("Could not mount {path}: real directory doesn't exist"),
"path:str",
fname
);
return false;
}
SDL_snprintf(path, sizeof(path), "%s/%s", real_dir, fname);
generateVirtualMountPath(virtualMountPath, sizeof(virtualMountPath));
if (!PHYSFS_mount(path, virtualMountPath, 0))
{
vlog_error(
"Error mounting %s: %s",
fname,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return false;
}
SDL_strlcpy(assetDir, path, sizeof(assetDir));
return true;
}
void FILESYSTEM_loadZip(const char* filename)
{
PHYSFS_File* zip = PHYSFS_openRead(filename);
if (zip == NULL)
{
vlog_error(
"Could not read zip %s: %s",
filename,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
}
if (!PHYSFS_mountHandle(zip, filename, "levels", 1))
{
vlog_error(
"Could not mount %s: %s",
filename,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
}
}
bool FILESYSTEM_mountAssets(const char* path)
{
const char* real_dir = PHYSFS_getRealDir(path);
if (real_dir != NULL &&
SDL_strncmp(real_dir, "levels/", sizeof("levels/") - 1) == 0 &&
endsWith(real_dir, ".zip"))
{
/* This is a level zip */
vlog_info("Asset directory is .zip at %s", real_dir);
if (!FILESYSTEM_mountAssetsFrom(real_dir))
{
return false;
}
MAYBE_FAIL(graphics.reloadresources());
}
else
{
/* If it's not a zip, look for a level folder */
char filename[MAX_PATH];
char virtual_path[MAX_PATH];
VVV_between(path, "levels/", filename, ".vvvvvv");
SDL_snprintf(
virtual_path,
sizeof(virtual_path),
"levels/%s/",
filename
);
if (FILESYSTEM_exists(virtual_path))
{
vlog_info("Asset directory exists at %s", virtual_path);
if (!FILESYSTEM_mountAssetsFrom(virtual_path))
{
return false;
}
MAYBE_FAIL(graphics.reloadresources());
}
else
{
/* Wasn't a level zip or folder! */
vlog_debug("Asset directory does not exist");
}
}
return true;
fail:
FILESYSTEM_unmountAssets();
return false;
}
void FILESYSTEM_unmountAssets(void)
{
if (assetDir[0] != '\0')
{
vlog_info("Unmounting %s", assetDir);
PHYSFS_unmount(assetDir);
assetDir[0] = '\0';
graphics.reloadresources();
}
else
{
vlog_debug("Cannot unmount when no asset directory is mounted");
}
}
static void getMountedPath(
char* buffer,
const size_t buffer_size,
const char* filename
) {
const char* path;
const bool assets_mounted = assetDir[0] != '\0';
char mounted_path[MAX_PATH];
if (assets_mounted)
{
SDL_snprintf(
mounted_path,
sizeof(mounted_path),
"%s%s",
virtualMountPath,
filename
);
}
if (assets_mounted && PHYSFS_exists(mounted_path))
{
path = mounted_path;
}
else
{
path = filename;
}
SDL_strlcpy(buffer, path, buffer_size);
}
bool FILESYSTEM_isAssetMounted(const char* filename)
{
const char* realDir;
char path[MAX_PATH];
/* Fast path */
if (assetDir[0] == '\0')
{
return false;
}
getMountedPath(path, sizeof(path), filename);
realDir = PHYSFS_getRealDir(path);
if (realDir == NULL)
{
return false;
}
return SDL_strcmp(assetDir, realDir) == 0;
}
bool FILESYSTEM_areAssetsInSameRealDir(const char* filenameA, const char* filenameB)
{
char pathA[MAX_PATH];
char pathB[MAX_PATH];
getMountedPath(pathA, sizeof(pathA), filenameA);
getMountedPath(pathB, sizeof(pathB), filenameB);
const char* realDirA = PHYSFS_getRealDir(pathA);
const char* realDirB = PHYSFS_getRealDir(pathB);
/* Both NULL, or both the same pointer? */
if (realDirA == realDirB)
{
return true;
}
if (realDirA == NULL || realDirB == NULL)
{
return false;
}
return SDL_strcmp(realDirA, realDirB) == 0;
}
static void load_stdin(void)
{
size_t pos = 0;
/* A .vvvvvv file with nothing is at least 140K...
* initial size of 1K shouldn't hurt. */
#define INITIAL_SIZE 1024
size_t alloc_size = INITIAL_SIZE;
stdin_buffer = (unsigned char*) SDL_malloc(INITIAL_SIZE);
#undef INITIAL_SIZE
if (stdin_buffer == NULL)
{
VVV_exit(1);
}
while (true)
{
int ch = fgetc(stdin);
bool end = ch == EOF;
if (end)
{
/* Always add null terminator. */
ch = '\0';
}
if (pos == alloc_size)
{
unsigned char *tmp;
alloc_size *= 2;
tmp = (unsigned char*) SDL_realloc((void*) stdin_buffer, alloc_size);
if (tmp == NULL)
{
VVV_exit(1);
}
stdin_buffer = tmp;
}
stdin_buffer[pos] = ch;
++pos;
if (end)
{
break;
}
}
stdin_length = pos - 1;
}
static PHYSFS_sint64 read_bytes(
const char* name, PHYSFS_File* handle, void* buffer,
const PHYSFS_uint64 length
) {
const PHYSFS_sint64 bytes_read = PHYSFS_readBytes(handle, buffer, length);
if (bytes_read < 0)
{
vlog_error(
"Could not read bytes from file %s: %s",
name,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
}
else if ((unsigned) bytes_read != length)
{
const char* reason;
if (PHYSFS_eof(handle))
{
reason = "Unexpected EOF";
}
else
{
reason = PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode());
}
vlog_warn(
"Partially read file %s: Expected %lli bytes, got %lli: %s",
name, length, bytes_read, reason
);
}
return bytes_read;
}
void FILESYSTEM_loadFileToMemory(
const char *name,
unsigned char **mem,
size_t *len
) {
PHYSFS_File *handle;
PHYSFS_sint64 length;
PHYSFS_sint64 bytes_read;
if (name == NULL || mem == NULL)
{
goto fail;
}
/* FIXME: Dumb hack to use `special/stdin.vvvvvv` here...
* This is also checked elsewhere... grep for `special/stdin`! */
if (SDL_strcmp(name, "levels/special/stdin.vvvvvv") == 0)
{
// this isn't *technically* necessary when piping directly from a file, but checking for that is annoying
if (stdin_buffer == NULL)
{
load_stdin();
}
*mem = (unsigned char*) SDL_malloc(stdin_length + 1); /* + 1 for null */
if (*mem == NULL)
{
VVV_exit(1);
}
if (len != NULL)
{
*len = stdin_length;
}
SDL_memcpy((void*) *mem, (void*) stdin_buffer, stdin_length + 1);
return;
}
handle = PHYSFS_openRead(name);
if (handle == NULL)
{
vlog_debug(
"Could not read file %s: %s",
name,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
goto fail;
}
length = PHYSFS_fileLength(handle);
if (len != NULL)
{
if (length < 0)
{
length = 0;
}
*len = length;
}
*mem = (unsigned char *) SDL_calloc(length + 1, 1);
if (*mem == NULL)
{
VVV_exit(1);
}
bytes_read = read_bytes(name, handle, *mem, length);
if (bytes_read < 0)
{
VVV_free(*mem);
}
PHYSFS_close(handle);
return;
fail:
if (mem != NULL)
{
*mem = NULL;
}
if (len != NULL)
{
*len = 0;
}
2020-01-01 21:29:24 +01:00
}
void FILESYSTEM_loadAssetToMemory(
const char* name,
unsigned char** mem,
size_t* len
) {
char path[MAX_PATH];
getMountedPath(path, sizeof(path), name);
FILESYSTEM_loadFileToMemory(path, mem, len);
}
bool FILESYSTEM_loadBinaryBlob(binaryBlob* blob, const char* filename)
{
PHYSFS_sint64 size;
PHYSFS_File* handle;
int valid, offset;
size_t i;
char path[MAX_PATH];
if (blob == NULL || filename == NULL)
{
return false;
}
getMountedPath(path, sizeof(path), filename);
handle = PHYSFS_openRead(path);
if (handle == NULL)
{
vlog_debug(
"Could not read binary blob %s: %s",
filename,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return false;
}
size = PHYSFS_fileLength(handle);
read_bytes(
filename,
handle,
&blob->m_headers,
sizeof(blob->m_headers)
);
valid = 0;
offset = sizeof(blob->m_headers);
for (i = 0; i < SDL_arraysize(blob->m_headers); ++i)
{
resourceheader* header = &blob->m_headers[i];
char** memblock = &blob->m_memblocks[i];
/* Name can be stupid, just needs to be terminated */
static const size_t last_char = sizeof(header->name) - 1;
if (header->name[last_char] != '\0')
{
vlog_warn(
"%s: Name of header %li is not null-terminated",
filename, i
);
}
header->name[last_char] = '\0';
if (header->valid & ~0x1 || !header->valid)
{
if (header->valid & ~0x1)
{
vlog_error(
"%s: Header %li's 'valid' value is invalid",
filename, i
);
}
goto fail; /* Must be EXACTLY 1 or 0 */
}
if (header->size < 1)
{
vlog_error(
"%s: Header %li's size value is zero or negative",
filename, i
);
goto fail; /* Must be nonzero and positive */
}
if (offset + header->size > size)
{
/* Not an error, VVVVVV 2.2 and below handled it gracefully */
vlog_warn(
"%s: Header %li's size value goes past end of file",
filename, i
);
}
PHYSFS_seek(handle, offset);
*memblock = (char*) SDL_malloc(header->size);
if (*memblock == NULL)
{
VVV_exit(1); /* Oh god we're out of memory, just bail */
}
offset += header->size;
header->size = read_bytes(filename, handle, *memblock, header->size);
valid += 1;
continue;
fail:
header->valid = false;
}
PHYSFS_close(handle);
if (valid == 0)
{
return false;
}
vlog_debug("The complete reloaded file size: %lli", size);
for (i = 0; i < SDL_arraysize(blob->m_headers); ++i)
{
const resourceheader* header = &blob->m_headers[i];
if (!header->valid)
{
continue;
}
vlog_debug("%s unpacked", header->name);
}
return true;
}
bool FILESYSTEM_saveTiXml2Document(const char *name, tinyxml2::XMLDocument& doc, bool sync /*= true*/)
{
if (!isInit)
{
vlog_warn("Filesystem not initialized! Not writing just to be safe.");
return false;
}
/* XMLDocument.SaveFile doesn't account for Unicode paths, PHYSFS does */
tinyxml2::XMLPrinter printer;
doc.Print(&printer);
PHYSFS_File* handle = PHYSFS_openWrite(name);
if (handle == NULL)
{
return false;
}
PHYSFS_writeBytes(handle, printer.CStr(), printer.CStrSize() - 1); // subtract one because CStrSize includes terminating null
PHYSFS_close(handle);
#ifdef __EMSCRIPTEN__
if (sync)
{
EM_ASM(FS.syncfs(false, function(err)
{
if (err)
{
console.warn("Error saving:", err);
alert("Error saving. Check console for more information.");
}
}));
}
#else
UNUSED(sync);
#endif
return true;
}
bool FILESYSTEM_loadTiXml2Document(const char *name, tinyxml2::XMLDocument& doc)
{
/* XMLDocument.LoadFile doesn't account for Unicode paths, PHYSFS does */
unsigned char *mem;
FILESYSTEM_loadFileToMemory(name, &mem, NULL);
if (mem == NULL)
{
return false;
}
doc.Parse((const char*) mem);
VVV_free(mem);
return true;
}
bool FILESYSTEM_loadAssetTiXml2Document(const char *name, tinyxml2::XMLDocument& doc)
{
/* Same as FILESYSTEM_loadTiXml2Document except for possible custom assets */
unsigned char *mem;
FILESYSTEM_loadAssetToMemory(name, &mem, NULL);
if (mem == NULL)
{
return false;
}
doc.Parse((const char*) mem);
VVV_free(mem);
return true;
}
struct CallbackWrapper
{
void (*callback)(const char* filename);
};
Refactor level dir listing to not use STL data marshalling Note that level dir listing still uses plenty of STL (including the end product - the `LevelMetaData` struct - which, for the purposes of 2.3, is okay enough (2.4 should remove STL usage entirely)); it's just that the initial act of iterating over the levels directory no longer takes four or SIX(!!!) heap allocations (not counting reallocations and other heap allocations this patch does not remove), and no longer does any data marshalling. Like text splitting, and binary blob extra indice grabbing, the current approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary std::vector of std::strings as a middleman to store all the filenames, and the game iterates over that std::vector to grab each level metadata. Except, it's even worse in this case, because PHYSFS_enumerateFiles() ALREADY does a heap allocation. Oh, and FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah, let me explain: 1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles(). 2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of chars on the heap. For each filename, it will: a. Allocate an array of chars for the filename. b. Reallocate the array of pointers to add the pointer to the above char array. (In this step, it also inserts the filename in alphabetically - without any further allocations, as far as I know - but this is a COMPLETELY unnecessary step, because we are going to sort the list of levels by ourselves via the metadata title in the end anyways.) 3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and allocates an std::vector on the heap to shove the list into. Then, for each filename, it will: a. Allocate an std::string, initialized to "levels/". b. Append the filename to the std::string above. This will most likely require a re-allocation. c. Duplicate the std::string - which requires allocating more memory again - to put it into the std::vector. (Compared to the PhysFS list above, the std::vector does less reallocations; it however will still end up reallocating a certain amount of times in the end.) 4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list. 5. Then to get the std::vector<std::string> back to the caller, we end up having to reallocate the std::vector again - reallocating every single std::string inside it, too - to give it back to the caller. And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed to either be called two times, or three times. This is because editorclass::getDirectoryData() will call editorclass::loadZips(), which will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call it AGAIN if a zip was found. Then once the function returns, getDirectoryData() will still unconditionally call FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting something on without regard for the whole picture of the system, but whatever; I can clean up their mess just fine. So, what do I do about this? Well, just like I did with text splitting and binary blob extras, make the final for-loop - the one that does the actual metadata parsing - more immediate. So how do I do that? Well, PhysFS has a function named PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function internally, and is basically just a wrapper with some allocation and alphabetization. PHYSFS_enumerate() takes in a pointer to a function, which it will call for every single entry that it iterates over. It also lets you pass in another arbitrary pointer that it leaves alone, which I use to pass through a function pointer that is the actual callback. So to clarify, there are two callbacks - one callback is passed through into another callback that gets passed through to PHYSFS_enumerate(). The callback that gets passed to PHYSFS_enumerate() is always the same, but the callback that gets passed through the callback can be different (if you look at the calling code, you can see that one caller passes through a normal level metadata callback; the other passes through a zip file callback). Furthermore, I've also cleaned it up so that if editorclass::loadZips() finds a zip file, it won't iterate over all the files in the levels directory a third time. Instead, the level directory only gets iterated over twice - once to check for zips, and another to load every level plus all zips; the second time is when all the heap allocations happen. And with that, level list loading now uses less STL templated stuff and much less heap allocations. Also, ed.directoryList basically has no reason to exist other than being a temporary std::vector, so I've removed it. This further decreases memory usage, depending on how many levels you have in your levels folder (I know that I usually have a lot and don't really ever clean it up, lol). Lastly, in the callback passed to PhysFS, `builtLocation` is actually no longer hardcoded to just the `levels` directory, since instead we now use the `origdir` variable that PhysFS passes us. So that's good, too.
2021-02-19 03:42:13 +01:00
static PHYSFS_EnumerateCallbackResult enumerateCallback(
void* data,
const char* origdir,
const char* filename
Refactor level dir listing to not use STL data marshalling Note that level dir listing still uses plenty of STL (including the end product - the `LevelMetaData` struct - which, for the purposes of 2.3, is okay enough (2.4 should remove STL usage entirely)); it's just that the initial act of iterating over the levels directory no longer takes four or SIX(!!!) heap allocations (not counting reallocations and other heap allocations this patch does not remove), and no longer does any data marshalling. Like text splitting, and binary blob extra indice grabbing, the current approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary std::vector of std::strings as a middleman to store all the filenames, and the game iterates over that std::vector to grab each level metadata. Except, it's even worse in this case, because PHYSFS_enumerateFiles() ALREADY does a heap allocation. Oh, and FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah, let me explain: 1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles(). 2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of chars on the heap. For each filename, it will: a. Allocate an array of chars for the filename. b. Reallocate the array of pointers to add the pointer to the above char array. (In this step, it also inserts the filename in alphabetically - without any further allocations, as far as I know - but this is a COMPLETELY unnecessary step, because we are going to sort the list of levels by ourselves via the metadata title in the end anyways.) 3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and allocates an std::vector on the heap to shove the list into. Then, for each filename, it will: a. Allocate an std::string, initialized to "levels/". b. Append the filename to the std::string above. This will most likely require a re-allocation. c. Duplicate the std::string - which requires allocating more memory again - to put it into the std::vector. (Compared to the PhysFS list above, the std::vector does less reallocations; it however will still end up reallocating a certain amount of times in the end.) 4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list. 5. Then to get the std::vector<std::string> back to the caller, we end up having to reallocate the std::vector again - reallocating every single std::string inside it, too - to give it back to the caller. And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed to either be called two times, or three times. This is because editorclass::getDirectoryData() will call editorclass::loadZips(), which will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call it AGAIN if a zip was found. Then once the function returns, getDirectoryData() will still unconditionally call FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting something on without regard for the whole picture of the system, but whatever; I can clean up their mess just fine. So, what do I do about this? Well, just like I did with text splitting and binary blob extras, make the final for-loop - the one that does the actual metadata parsing - more immediate. So how do I do that? Well, PhysFS has a function named PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function internally, and is basically just a wrapper with some allocation and alphabetization. PHYSFS_enumerate() takes in a pointer to a function, which it will call for every single entry that it iterates over. It also lets you pass in another arbitrary pointer that it leaves alone, which I use to pass through a function pointer that is the actual callback. So to clarify, there are two callbacks - one callback is passed through into another callback that gets passed through to PHYSFS_enumerate(). The callback that gets passed to PHYSFS_enumerate() is always the same, but the callback that gets passed through the callback can be different (if you look at the calling code, you can see that one caller passes through a normal level metadata callback; the other passes through a zip file callback). Furthermore, I've also cleaned it up so that if editorclass::loadZips() finds a zip file, it won't iterate over all the files in the levels directory a third time. Instead, the level directory only gets iterated over twice - once to check for zips, and another to load every level plus all zips; the second time is when all the heap allocations happen. And with that, level list loading now uses less STL templated stuff and much less heap allocations. Also, ed.directoryList basically has no reason to exist other than being a temporary std::vector, so I've removed it. This further decreases memory usage, depending on how many levels you have in your levels folder (I know that I usually have a lot and don't really ever clean it up, lol). Lastly, in the callback passed to PhysFS, `builtLocation` is actually no longer hardcoded to just the `levels` directory, since instead we now use the `origdir` variable that PhysFS passes us. So that's good, too.
2021-02-19 03:42:13 +01:00
) {
struct CallbackWrapper* wrapper = (struct CallbackWrapper*) data;
void (*callback)(const char*) = wrapper->callback;
char builtLocation[MAX_PATH];
Refactor level dir listing to not use STL data marshalling Note that level dir listing still uses plenty of STL (including the end product - the `LevelMetaData` struct - which, for the purposes of 2.3, is okay enough (2.4 should remove STL usage entirely)); it's just that the initial act of iterating over the levels directory no longer takes four or SIX(!!!) heap allocations (not counting reallocations and other heap allocations this patch does not remove), and no longer does any data marshalling. Like text splitting, and binary blob extra indice grabbing, the current approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary std::vector of std::strings as a middleman to store all the filenames, and the game iterates over that std::vector to grab each level metadata. Except, it's even worse in this case, because PHYSFS_enumerateFiles() ALREADY does a heap allocation. Oh, and FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah, let me explain: 1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles(). 2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of chars on the heap. For each filename, it will: a. Allocate an array of chars for the filename. b. Reallocate the array of pointers to add the pointer to the above char array. (In this step, it also inserts the filename in alphabetically - without any further allocations, as far as I know - but this is a COMPLETELY unnecessary step, because we are going to sort the list of levels by ourselves via the metadata title in the end anyways.) 3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and allocates an std::vector on the heap to shove the list into. Then, for each filename, it will: a. Allocate an std::string, initialized to "levels/". b. Append the filename to the std::string above. This will most likely require a re-allocation. c. Duplicate the std::string - which requires allocating more memory again - to put it into the std::vector. (Compared to the PhysFS list above, the std::vector does less reallocations; it however will still end up reallocating a certain amount of times in the end.) 4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list. 5. Then to get the std::vector<std::string> back to the caller, we end up having to reallocate the std::vector again - reallocating every single std::string inside it, too - to give it back to the caller. And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed to either be called two times, or three times. This is because editorclass::getDirectoryData() will call editorclass::loadZips(), which will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call it AGAIN if a zip was found. Then once the function returns, getDirectoryData() will still unconditionally call FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting something on without regard for the whole picture of the system, but whatever; I can clean up their mess just fine. So, what do I do about this? Well, just like I did with text splitting and binary blob extras, make the final for-loop - the one that does the actual metadata parsing - more immediate. So how do I do that? Well, PhysFS has a function named PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function internally, and is basically just a wrapper with some allocation and alphabetization. PHYSFS_enumerate() takes in a pointer to a function, which it will call for every single entry that it iterates over. It also lets you pass in another arbitrary pointer that it leaves alone, which I use to pass through a function pointer that is the actual callback. So to clarify, there are two callbacks - one callback is passed through into another callback that gets passed through to PHYSFS_enumerate(). The callback that gets passed to PHYSFS_enumerate() is always the same, but the callback that gets passed through the callback can be different (if you look at the calling code, you can see that one caller passes through a normal level metadata callback; the other passes through a zip file callback). Furthermore, I've also cleaned it up so that if editorclass::loadZips() finds a zip file, it won't iterate over all the files in the levels directory a third time. Instead, the level directory only gets iterated over twice - once to check for zips, and another to load every level plus all zips; the second time is when all the heap allocations happen. And with that, level list loading now uses less STL templated stuff and much less heap allocations. Also, ed.directoryList basically has no reason to exist other than being a temporary std::vector, so I've removed it. This further decreases memory usage, depending on how many levels you have in your levels folder (I know that I usually have a lot and don't really ever clean it up, lol). Lastly, in the callback passed to PhysFS, `builtLocation` is actually no longer hardcoded to just the `levels` directory, since instead we now use the `origdir` variable that PhysFS passes us. So that's good, too.
2021-02-19 03:42:13 +01:00
SDL_snprintf(
builtLocation,
sizeof(builtLocation),
"%s/%s",
origdir,
filename
);
2020-01-01 21:29:24 +01:00
callback(builtLocation);
2020-01-01 21:29:24 +01:00
return PHYSFS_ENUM_OK;
Refactor level dir listing to not use STL data marshalling Note that level dir listing still uses plenty of STL (including the end product - the `LevelMetaData` struct - which, for the purposes of 2.3, is okay enough (2.4 should remove STL usage entirely)); it's just that the initial act of iterating over the levels directory no longer takes four or SIX(!!!) heap allocations (not counting reallocations and other heap allocations this patch does not remove), and no longer does any data marshalling. Like text splitting, and binary blob extra indice grabbing, the current approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary std::vector of std::strings as a middleman to store all the filenames, and the game iterates over that std::vector to grab each level metadata. Except, it's even worse in this case, because PHYSFS_enumerateFiles() ALREADY does a heap allocation. Oh, and FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah, let me explain: 1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles(). 2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of chars on the heap. For each filename, it will: a. Allocate an array of chars for the filename. b. Reallocate the array of pointers to add the pointer to the above char array. (In this step, it also inserts the filename in alphabetically - without any further allocations, as far as I know - but this is a COMPLETELY unnecessary step, because we are going to sort the list of levels by ourselves via the metadata title in the end anyways.) 3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and allocates an std::vector on the heap to shove the list into. Then, for each filename, it will: a. Allocate an std::string, initialized to "levels/". b. Append the filename to the std::string above. This will most likely require a re-allocation. c. Duplicate the std::string - which requires allocating more memory again - to put it into the std::vector. (Compared to the PhysFS list above, the std::vector does less reallocations; it however will still end up reallocating a certain amount of times in the end.) 4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list. 5. Then to get the std::vector<std::string> back to the caller, we end up having to reallocate the std::vector again - reallocating every single std::string inside it, too - to give it back to the caller. And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed to either be called two times, or three times. This is because editorclass::getDirectoryData() will call editorclass::loadZips(), which will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call it AGAIN if a zip was found. Then once the function returns, getDirectoryData() will still unconditionally call FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting something on without regard for the whole picture of the system, but whatever; I can clean up their mess just fine. So, what do I do about this? Well, just like I did with text splitting and binary blob extras, make the final for-loop - the one that does the actual metadata parsing - more immediate. So how do I do that? Well, PhysFS has a function named PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function internally, and is basically just a wrapper with some allocation and alphabetization. PHYSFS_enumerate() takes in a pointer to a function, which it will call for every single entry that it iterates over. It also lets you pass in another arbitrary pointer that it leaves alone, which I use to pass through a function pointer that is the actual callback. So to clarify, there are two callbacks - one callback is passed through into another callback that gets passed through to PHYSFS_enumerate(). The callback that gets passed to PHYSFS_enumerate() is always the same, but the callback that gets passed through the callback can be different (if you look at the calling code, you can see that one caller passes through a normal level metadata callback; the other passes through a zip file callback). Furthermore, I've also cleaned it up so that if editorclass::loadZips() finds a zip file, it won't iterate over all the files in the levels directory a third time. Instead, the level directory only gets iterated over twice - once to check for zips, and another to load every level plus all zips; the second time is when all the heap allocations happen. And with that, level list loading now uses less STL templated stuff and much less heap allocations. Also, ed.directoryList basically has no reason to exist other than being a temporary std::vector, so I've removed it. This further decreases memory usage, depending on how many levels you have in your levels folder (I know that I usually have a lot and don't really ever clean it up, lol). Lastly, in the callback passed to PhysFS, `builtLocation` is actually no longer hardcoded to just the `levels` directory, since instead we now use the `origdir` variable that PhysFS passes us. So that's good, too.
2021-02-19 03:42:13 +01:00
}
2020-01-01 21:29:24 +01:00
Refactor level dir listing to not use STL data marshalling Note that level dir listing still uses plenty of STL (including the end product - the `LevelMetaData` struct - which, for the purposes of 2.3, is okay enough (2.4 should remove STL usage entirely)); it's just that the initial act of iterating over the levels directory no longer takes four or SIX(!!!) heap allocations (not counting reallocations and other heap allocations this patch does not remove), and no longer does any data marshalling. Like text splitting, and binary blob extra indice grabbing, the current approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary std::vector of std::strings as a middleman to store all the filenames, and the game iterates over that std::vector to grab each level metadata. Except, it's even worse in this case, because PHYSFS_enumerateFiles() ALREADY does a heap allocation. Oh, and FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah, let me explain: 1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles(). 2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of chars on the heap. For each filename, it will: a. Allocate an array of chars for the filename. b. Reallocate the array of pointers to add the pointer to the above char array. (In this step, it also inserts the filename in alphabetically - without any further allocations, as far as I know - but this is a COMPLETELY unnecessary step, because we are going to sort the list of levels by ourselves via the metadata title in the end anyways.) 3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and allocates an std::vector on the heap to shove the list into. Then, for each filename, it will: a. Allocate an std::string, initialized to "levels/". b. Append the filename to the std::string above. This will most likely require a re-allocation. c. Duplicate the std::string - which requires allocating more memory again - to put it into the std::vector. (Compared to the PhysFS list above, the std::vector does less reallocations; it however will still end up reallocating a certain amount of times in the end.) 4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list. 5. Then to get the std::vector<std::string> back to the caller, we end up having to reallocate the std::vector again - reallocating every single std::string inside it, too - to give it back to the caller. And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed to either be called two times, or three times. This is because editorclass::getDirectoryData() will call editorclass::loadZips(), which will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call it AGAIN if a zip was found. Then once the function returns, getDirectoryData() will still unconditionally call FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting something on without regard for the whole picture of the system, but whatever; I can clean up their mess just fine. So, what do I do about this? Well, just like I did with text splitting and binary blob extras, make the final for-loop - the one that does the actual metadata parsing - more immediate. So how do I do that? Well, PhysFS has a function named PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function internally, and is basically just a wrapper with some allocation and alphabetization. PHYSFS_enumerate() takes in a pointer to a function, which it will call for every single entry that it iterates over. It also lets you pass in another arbitrary pointer that it leaves alone, which I use to pass through a function pointer that is the actual callback. So to clarify, there are two callbacks - one callback is passed through into another callback that gets passed through to PHYSFS_enumerate(). The callback that gets passed to PHYSFS_enumerate() is always the same, but the callback that gets passed through the callback can be different (if you look at the calling code, you can see that one caller passes through a normal level metadata callback; the other passes through a zip file callback). Furthermore, I've also cleaned it up so that if editorclass::loadZips() finds a zip file, it won't iterate over all the files in the levels directory a third time. Instead, the level directory only gets iterated over twice - once to check for zips, and another to load every level plus all zips; the second time is when all the heap allocations happen. And with that, level list loading now uses less STL templated stuff and much less heap allocations. Also, ed.directoryList basically has no reason to exist other than being a temporary std::vector, so I've removed it. This further decreases memory usage, depending on how many levels you have in your levels folder (I know that I usually have a lot and don't really ever clean it up, lol). Lastly, in the callback passed to PhysFS, `builtLocation` is actually no longer hardcoded to just the `levels` directory, since instead we now use the `origdir` variable that PhysFS passes us. So that's good, too.
2021-02-19 03:42:13 +01:00
void FILESYSTEM_enumerateLevelDirFileNames(
void (*callback)(const char* filename)
Refactor level dir listing to not use STL data marshalling Note that level dir listing still uses plenty of STL (including the end product - the `LevelMetaData` struct - which, for the purposes of 2.3, is okay enough (2.4 should remove STL usage entirely)); it's just that the initial act of iterating over the levels directory no longer takes four or SIX(!!!) heap allocations (not counting reallocations and other heap allocations this patch does not remove), and no longer does any data marshalling. Like text splitting, and binary blob extra indice grabbing, the current approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary std::vector of std::strings as a middleman to store all the filenames, and the game iterates over that std::vector to grab each level metadata. Except, it's even worse in this case, because PHYSFS_enumerateFiles() ALREADY does a heap allocation. Oh, and FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah, let me explain: 1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles(). 2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of chars on the heap. For each filename, it will: a. Allocate an array of chars for the filename. b. Reallocate the array of pointers to add the pointer to the above char array. (In this step, it also inserts the filename in alphabetically - without any further allocations, as far as I know - but this is a COMPLETELY unnecessary step, because we are going to sort the list of levels by ourselves via the metadata title in the end anyways.) 3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and allocates an std::vector on the heap to shove the list into. Then, for each filename, it will: a. Allocate an std::string, initialized to "levels/". b. Append the filename to the std::string above. This will most likely require a re-allocation. c. Duplicate the std::string - which requires allocating more memory again - to put it into the std::vector. (Compared to the PhysFS list above, the std::vector does less reallocations; it however will still end up reallocating a certain amount of times in the end.) 4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list. 5. Then to get the std::vector<std::string> back to the caller, we end up having to reallocate the std::vector again - reallocating every single std::string inside it, too - to give it back to the caller. And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed to either be called two times, or three times. This is because editorclass::getDirectoryData() will call editorclass::loadZips(), which will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call it AGAIN if a zip was found. Then once the function returns, getDirectoryData() will still unconditionally call FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting something on without regard for the whole picture of the system, but whatever; I can clean up their mess just fine. So, what do I do about this? Well, just like I did with text splitting and binary blob extras, make the final for-loop - the one that does the actual metadata parsing - more immediate. So how do I do that? Well, PhysFS has a function named PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function internally, and is basically just a wrapper with some allocation and alphabetization. PHYSFS_enumerate() takes in a pointer to a function, which it will call for every single entry that it iterates over. It also lets you pass in another arbitrary pointer that it leaves alone, which I use to pass through a function pointer that is the actual callback. So to clarify, there are two callbacks - one callback is passed through into another callback that gets passed through to PHYSFS_enumerate(). The callback that gets passed to PHYSFS_enumerate() is always the same, but the callback that gets passed through the callback can be different (if you look at the calling code, you can see that one caller passes through a normal level metadata callback; the other passes through a zip file callback). Furthermore, I've also cleaned it up so that if editorclass::loadZips() finds a zip file, it won't iterate over all the files in the levels directory a third time. Instead, the level directory only gets iterated over twice - once to check for zips, and another to load every level plus all zips; the second time is when all the heap allocations happen. And with that, level list loading now uses less STL templated stuff and much less heap allocations. Also, ed.directoryList basically has no reason to exist other than being a temporary std::vector, so I've removed it. This further decreases memory usage, depending on how many levels you have in your levels folder (I know that I usually have a lot and don't really ever clean it up, lol). Lastly, in the callback passed to PhysFS, `builtLocation` is actually no longer hardcoded to just the `levels` directory, since instead we now use the `origdir` variable that PhysFS passes us. So that's good, too.
2021-02-19 03:42:13 +01:00
) {
int success;
struct CallbackWrapper wrapper = {callback};
success = PHYSFS_enumerate("levels", enumerateCallback, (void*) &wrapper);
if (success == 0)
{
vlog_error(
"Could not get list of levels: %s",
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
}
2020-01-01 21:29:24 +01:00
}
const char* FILESYSTEM_enumerate(const char* folder, EnumHandle* handle)
{
/* List all files in a folder with PHYSFS_enumerateFiles.
*
* Doing it this way means we can decide and filter
* what's in the lists (in wrapper functions).
*
* Called like this:
*
* EnumHandle handle = {};
* const char* item;
* while ((item = FILESYSTEM_enumerate("graphics", &handle)) != NULL)
* {
* puts(item);
* }
* FILESYSTEM_freeEnumerate(&handle);
*/
if (handle->physfs_list == NULL)
{
// First iteration, set things up
handle->physfs_list = PHYSFS_enumerateFiles(folder);
handle->_item = handle->physfs_list;
}
/* Return the next item, and increment the pointer.
* (once we return NULL, handle->_item points to 1 past end of array) */
return *(handle->_item++);
}
const char* FILESYSTEM_enumerateAssets(const char* folder, EnumHandle* handle)
{
/* This function enumerates ONLY level-specific assets.
* If there are only global assets and no level-specific ones,
* we want an empty list.
*
* This function is called the same way as FILESYSTEM_enumerate, see above. */
if (!FILESYSTEM_isAssetMounted(folder))
{
return NULL;
}
char mounted_path[MAX_PATH];
getMountedPath(mounted_path, sizeof(mounted_path), folder);
const char* item;
while ((item = FILESYSTEM_enumerate(mounted_path, handle)) != NULL)
{
char full_name[128];
SDL_snprintf(full_name, sizeof(full_name), "%s/%s", mounted_path, item);
if (FILESYSTEM_isFile(full_name) && item[0] != '.')
{
return item;
}
}
return NULL;
}
const char* FILESYSTEM_enumerateLanguageCodes(EnumHandle* handle)
{
/* This function enumerates all the language codes.
*
* This function is called the same way as FILESYSTEM_enumerate, see above. */
const char* item;
while ((item = FILESYSTEM_enumerate("lang", handle)) != NULL)
{
char full_name[128];
SDL_snprintf(full_name, sizeof(full_name), "lang/%s", item);
if (FILESYSTEM_isDirectory(full_name) && item[0] != '.')
{
return item;
}
}
return NULL;
}
void FILESYSTEM_freeEnumerate(EnumHandle* handle)
{
/* Call this function after enumerating with FILESYSTEM_enumerate or friends. */
if (handle == NULL)
{
return;
}
PHYSFS_freeList(handle->physfs_list);
}
static int PLATFORM_getOSDirectory(char* output, const size_t output_size)
2020-01-01 21:29:24 +01:00
{
#ifdef _WIN32
/* This block is here for compatibility, do not touch it! */
WCHAR utf16_path[MAX_PATH];
HRESULT retcode = SHGetFolderPathW(
NULL,
CSIDL_PERSONAL,
NULL,
SHGFP_TYPE_CURRENT,
utf16_path
);
int num_bytes;
if (FAILED(retcode))
{
vlog_error(
"Could not get OS directory: SHGetFolderPathW returned 0x%08x",
retcode
);
return 0;
}
num_bytes = WideCharToMultiByte(
CP_UTF8,
0,
utf16_path,
-1,
output,
output_size,
NULL,
NULL
);
if (num_bytes == 0)
{
vlog_error(
"Could not get OS directory: UTF-8 conversion failed with %d",
GetLastError()
);
return 0;
}
SDL_strlcat(output, "\\VVVVVV\\", MAX_PATH);
mkdir(output, 0777);
return 1;
2020-01-01 21:29:24 +01:00
#else
const char* prefDir = PHYSFS_getPrefDir("distractionware", "VVVVVV");
if (prefDir == NULL)
{
vlog_error(
"Could not get OS directory: %s",
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
return 0;
}
SDL_strlcpy(output, prefDir, output_size);
return 1;
2020-01-01 21:29:24 +01:00
#endif
}
bool FILESYSTEM_openDirectoryEnabled(void)
{
return !gameScreen.isForcedFullscreen();
}
2021-03-31 20:01:58 +02:00
#if defined(__EMSCRIPTEN__)
bool FILESYSTEM_openDirectory(const char *dname)
{
return false;
2021-03-31 20:01:58 +02:00
}
#else
bool FILESYSTEM_openDirectory(const char *dname)
{
char url[MAX_PATH];
SDL_snprintf(url, sizeof(url), "file://%s", dname);
if (SDL_OpenURL(url) == -1)
{
vlog_error("Error opening directory: %s", SDL_GetError());
return false;
}
return true;
}
2021-03-31 20:01:58 +02:00
#endif
bool FILESYSTEM_delete(const char *name)
{
return PHYSFS_delete(name) != 0;
}
static void levelSaveCallback(const char* filename)
{
if (endsWith(filename, ".vvvvvv.vvv"))
{
if (!FILESYSTEM_delete(filename))
{
vlog_error("Error deleting %s", filename);
}
}
}
void FILESYSTEM_deleteLevelSaves(void)
{
int success;
struct CallbackWrapper wrapper = {levelSaveCallback};
success = PHYSFS_enumerate(
"saves",
enumerateCallback,
(void*) &wrapper
);
if (success == 0)
{
vlog_error(
"Could not enumerate saves/: %s",
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
);
}
}