WIP virtual filesystem

This commit is contained in:
Andrei "Akari" Alexeyev 2017-04-18 22:48:18 +03:00
parent 02f1c935ff
commit 471f30083e
49 changed files with 1496 additions and 348 deletions

View file

@ -11,7 +11,6 @@ else()
endif()
option(RELATIVE "Use only relative paths to the executable and install everything in the same directory." ${RELATIVE_DEFAULT})
option(USE_SDL2_PATHS "Use the SDL2 filesystem API to determine where to save settings." ON)
option(NO_AUDIO "Build without audio support" OFF)
option(DEBUG_USE_UBSAN "Enable the Undefined Behaviour Sanitizer (UBSan) in debug builds. Only disable if the compiler or target platform doesn't support it." ON)
option(DEBUG_USE_ASAN "Enable the Address Sanitizer (ASan) and leak detection in debug builds." OFF)

View file

@ -87,16 +87,14 @@ set(SRCs
rwops/rwops_zlib.c
rwops/rwops_segment.c
rwops/rwops_autobuf.c
vfs/private.c
vfs/public.c
vfs/pathutil.c
vfs/syspath_posix.c # TODO: write a win32 version of this
vfs/union.c
vfs/vdir.c
)
if(USE_SDL2_PATHS)
set(SRCs ${SRCs} paths/sdl.c)
elseif(RELATIVE)
set(SRCs ${SRCs} paths/relative.c)
else()
set(SRCs ${SRCs} paths/static.c)
endif()
if(NO_AUDIO)
set(SRCs ${SRCs}
audio_null.c

View file

@ -67,9 +67,7 @@ static void bgm_cfg_volume_callback(ConfigIndex idx, ConfigValue v) {
}
static void load_bgm_descriptions(void) {
char *fullname = strjoin(get_prefix(), "bgm/bgm.conf", NULL);
bgm_descriptions = parse_keyvalue_file(fullname, 16);
free(fullname);
bgm_descriptions = parse_keyvalue_file(BGM_PATH_PREFIX "bgm.conf", HT_DYNAMIC_SIZE);
return;
}

View file

@ -6,8 +6,6 @@
*/
#include <SDL_mixer.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "audio.h"
#include "global.h"
@ -111,9 +109,8 @@ void audio_backend_set_bgm_volume(float gain) {
char* audio_mixer_sound_path(const char *prefix, const char *name) {
for(const char **ext = mixer_audio_exts; *ext; ++ext) {
char *p = strjoin(prefix, name, *ext, NULL);
struct stat statbuf;
if(!stat(p, &statbuf)) {
if(vfs_query(p).exists) {
return p;
}

View file

@ -41,6 +41,7 @@ int cli_args(int argc, char **argv, CLIAction *a) {
{{"shotmode", required_argument, 0, 's'}, "Select a shotmode (marisaA/youmuA/marisaB/youmuB)", "SMODE"},
{{"frameskip", optional_argument, 0, 'f'}, "Disable FPS limiter, render only every %s frame", "FRAME"},
{{"dumpstages", no_argument, 0, 'u'}, "Print a list of all stages in the game", 0},
{{"vfs-tree", required_argument, 0, 't'}, "Print the virtual filesystem tree starting from %s", "PATH"},
#endif
{{"help", no_argument, 0, 'h'}, "Display this help."},
{{0,0,0,0},0,0}
@ -144,6 +145,10 @@ int cli_args(int argc, char **argv, CLIAction *a) {
}
}
break;
case 't':
a->type = CLI_DumpVFSTree,
a->filename = strdup(optarg ? optarg : "");
break;
default:
log_fatal("Unknown option (this shouldnt happen)");
}

View file

@ -8,6 +8,7 @@ typedef enum {
CLI_PlayReplay,
CLI_SelectStage,
CLI_DumpStages,
CLI_DumpVFSTree,
CLI_Quit,
} CLIActionType;

View file

@ -10,7 +10,6 @@
#include "config.h"
#include "global.h"
#include "paths/native.h"
static bool config_initialized = false;
@ -263,25 +262,18 @@ static void config_delete_unknown_entries(void) {
delete_all_elements((void**)&unknowndefs, config_delete_unknown_entry);
}
static char* config_path(const char *filename) {
return strjoin(get_config_path(), "/", filename, NULL);
}
static SDL_RWops* config_open(const char *filename, const char *mode) {
char *buf = config_path(filename);
SDL_RWops *out = SDL_RWFromFile(buf, mode);
free(buf);
static SDL_RWops* config_open(const char *filename, VFSOpenMode mode) {
SDL_RWops *out = vfs_open(CONFIG_FILE, mode);
if(!out) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
log_warn("VFS error: %s", vfs_get_error());
}
return out;
}
void config_save(const char *filename) {
SDL_RWops *out = config_open(filename, "w");
void config_save(void) {
SDL_RWops *out = config_open(CONFIG_FILE, VFS_MODE_WRITE);
ConfigEntry *e = configdefs;
if(!out)
@ -317,7 +309,10 @@ void config_save(const char *filename) {
}
SDL_RWclose(out);
log_info("Saved config '%s'", filename);
char *sp = vfs_syspath_or_repr(CONFIG_FILE);
log_info("Saved config '%s'", sp);
free(sp);
}
#define INTOF(s) ((int)strtol(s, NULL, 10))
@ -362,12 +357,14 @@ static void config_set(const char *key, const char *val, void *data) {
#undef INTOF
#undef FLOATOF
void config_load(const char *filename) {
char *path = config_path(filename);
void config_load(void) {
config_init();
log_info("Loading configuration from %s", path);
if(!parse_keyvalue_file_cb(path, config_set, NULL)) {
char *sp = vfs_syspath_or_repr(CONFIG_FILE);
log_info("Loading configuration from %s", sp);
free(sp);
if(!parse_keyvalue_file_cb(CONFIG_FILE, config_set, NULL)) {
log_warn("Errors occured while parsing the configuration file");
}
free(path);
}

View file

@ -12,6 +12,8 @@
#include <SDL_keycode.h>
#include <stdbool.h>
#define CONFIG_FILE "storage/config"
/*
Define these macros, then use CONFIGDEFS to expand them all for all config entries, or KEYDEFS for just keybindings.
Don't forget to undef them afterwards.
@ -180,8 +182,8 @@ KeyIndex config_key_from_gamepad_button(int btn);
void config_reset(void);
void config_init(void);
void config_uninit(void);
void config_load(const char *filename);
void config_save(const char *filename);
void config_load(void);
void config_save(void);
void config_set_callback(ConfigIndex idx, ConfigCallback callback);
#ifndef DEBUG

View file

@ -46,9 +46,6 @@
#include "rwops/all.h"
#include "cli.h"
#define FILE_PREFIX PREFIX "/share/taisei/"
#define CONFIG_FILE "config"
enum {
// defaults
RESX = 800,

View file

@ -5,8 +5,6 @@
* Copyright (C) 2011, Lukas Weber <laochailan@web.de>
*/
#include <sys/stat.h>
#include <errno.h>
#include <locale.h>
#include "global.h"
@ -14,18 +12,20 @@
#include "audio.h"
#include "stage.h"
#include "menu/mainmenu.h"
#include "paths/native.h"
#include "gamepad.h"
#include "resource/bgm.h"
#include "progress.h"
#include "hashtable.h"
#include "log.h"
#include "cli.h"
#include "vfs/public.h"
void taisei_shutdown(void) {
#define STATIC_RESOURCE_PREFIX PREFIX "/share/taisei/"
static void taisei_shutdown(void) {
log_info("Shutting down");
config_save(CONFIG_FILE);
config_save();
progress_save();
progress_unload();
@ -37,6 +37,7 @@ void taisei_shutdown(void) {
gamepad_shutdown();
stage_free_array();
config_uninit();
vfs_uninit();
log_info("Good bye");
SDL_Quit();
@ -44,25 +45,23 @@ void taisei_shutdown(void) {
log_shutdown();
}
void init_log(void) {
const char *pref = get_config_path();
char *logpath = strfmt("%s/%s", pref, "log.txt");
static void init_log(void) {
LogLevel lvls_console = log_parse_levels(LOG_DEFAULT_LEVELS_CONSOLE, getenv("TAISEI_LOGLVLS_CONSOLE"));
LogLevel lvls_stdout = lvls_console & log_parse_levels(LOG_DEFAULT_LEVELS_STDOUT, getenv("TAISEI_LOGLVLS_STDOUT"));
LogLevel lvls_stderr = lvls_console & log_parse_levels(LOG_DEFAULT_LEVELS_STDERR, getenv("TAISEI_LOGLVLS_STDERR"));
LogLevel lvls_file = log_parse_levels(LOG_DEFAULT_LEVELS_FILE, getenv("TAISEI_LOGLVLS_FILE"));
LogLevel lvls_backtrace = log_parse_levels(LOG_DEFAULT_LEVELS_BACKTRACE, getenv("TAISEI_LOGLVLS_BACKTRACE"));
log_init(LOG_DEFAULT_LEVELS, lvls_backtrace);
log_add_output(lvls_stdout, SDL_RWFromFP(stdout, false));
log_add_output(lvls_stderr, SDL_RWFromFP(stderr, false));
log_add_output(lvls_file, SDL_RWFromFile(logpath, "w"));
free(logpath);
}
int run_tests(void) {
static void init_log_file(void) {
LogLevel lvls_file = log_parse_levels(LOG_DEFAULT_LEVELS_FILE, getenv("TAISEI_LOGLVLS_FILE"));
log_add_output(lvls_file, vfs_open("storage/log.txt", VFS_MODE_WRITE));
}
static int run_tests(void) {
if(tsrand_test()) {
return 1;
}
@ -101,6 +100,54 @@ static void init_sdl(void) {
log_info("Using SDL %u.%u.%u", v.major, v.minor, v.patch);
}
static void get_core_paths(char **res, char **storage) {
#ifdef RELATIVE
*res = SDL_GetBasePath();
strappend(res, "data/");
#else
*res = strdup(STATIC_RESOURCE_PREFIX);
#endif
*storage = SDL_GetPrefPath("", "taisei");
}
static void init_vfs(bool silent) {
char *res_path, *storage_path;
get_core_paths(&res_path, &storage_path);
if(!silent) {
log_info("Resource path: %s", res_path);
log_info("Storage path: %s", storage_path);
}
char *p = NULL;
struct mpoint_t {
const char *dest; const char *syspath; bool mkdir;
} mpts[] = {
{"storage", storage_path, true},
{"res", res_path, false},
{"res", p = strfmt("%s/resources", storage_path), true},
{NULL}
};
vfs_init();
vfs_create_union_mountpoint("res");
for(struct mpoint_t *mp = mpts; mp->dest; ++mp) {
if(!vfs_mount_syspath(mp->dest, mp->syspath, mp->mkdir)) {
log_fatal("Failed to mount '%s': %s", mp->dest, vfs_get_error());
}
}
vfs_mkdir_required("storage/replays");
vfs_mkdir_required("storage/screenshots");
free(p);
free(res_path);
free(storage_path);
}
static void log_lib_versions(void) {
log_info("Compiled against zlib %s", ZLIB_VERSION);
log_info("Using zlib %s", zlibVersion());
@ -114,7 +161,6 @@ int main(int argc, char **argv) {
Replay replay = {0};
int replay_idx = 0;
init_paths();
init_log();
if(run_tests()) {
@ -136,7 +182,7 @@ int main(int argc, char **argv) {
}
return 0;
} else if(a.type == CLI_PlayReplay) {
if(!replay_load(&replay, a.filename, REPLAY_READ_ALL | REPLAY_READ_RAWPATH)) {
if(!replay_load_syspath(&replay, a.filename, REPLAY_READ_ALL)) {
return 1;
}
@ -144,26 +190,30 @@ int main(int argc, char **argv) {
if(replay_idx < 0) {
return 1;
}
} else if(a.type == CLI_DumpVFSTree) {
init_vfs(true);
SDL_RWops *rwops = SDL_RWFromFP(stdout, false);
if(!rwops) {
log_fatal("SDL_RWFromFP() failed: %s", SDL_GetError());
}
if(!vfs_print_tree(rwops, a.filename)) {
log_warn("VFS error: %s", vfs_get_error());
SDL_RWclose(rwops);
return 1;
}
SDL_RWclose(rwops);
return 0;
}
free_cli_action(&a);
init_vfs(false);
init_log_file();
log_info("Content path: %s", get_prefix());
log_info("Userdata path: %s", get_config_path());
MKDIR(get_config_path());
MKDIR(get_screenshots_path());
MKDIR(get_replays_path());
if(chdir(get_prefix())) {
log_fatal("chdir() failed: %s", strerror(errno));
} else {
char cwd[1024]; // i don't care if this is not enough for you, getcwd is garbage
getcwd(cwd, sizeof(cwd));
log_info("Changed working directory to %s", cwd);
}
config_load(CONFIG_FILE);
config_load();
log_lib_versions();

View file

@ -18,7 +18,6 @@
#include "global.h"
#include "video.h"
#include "stage.h"
#include "paths/native.h"
void enter_options(MenuData *menu, void *arg) {
MenuData m;

View file

@ -12,7 +12,6 @@
#include "options.h"
#include "global.h"
#include "video.h"
#include "paths/native.h"
// --- Menu entry <-> config option binding stuff --- //

View file

@ -6,14 +6,12 @@
* Copyright (C) 2012, Alexeyew Andrew <https://github.com/nexAkari>
*/
#include <dirent.h>
#include <time.h>
#include "global.h"
#include "menu.h"
#include "options.h"
#include "mainmenu.h"
#include "replayview.h"
#include "paths/native.h"
#include "plrmodes.h"
#include "video.h"
#include "common.h"
@ -257,24 +255,24 @@ int replayview_cmp(const void *a, const void *b) {
}
int fill_replayview_menu(MenuData *m) {
DIR *dir = opendir(get_replays_path());
struct dirent *e;
VFSDir *dir = vfs_dir_open("storage/replays");
const char *filename;
int rpys = 0;
if(!dir) {
log_warn("Could't read %s", get_replays_path());
log_warn("VFS error: %s", vfs_get_error());
return -1;
}
char ext[5];
snprintf(ext, 5, ".%s", REPLAY_EXTENSION);
while((e = readdir(dir))) {
if(!strendswith(e->d_name, ext))
while((filename = vfs_dir_read(dir))) {
if(!strendswith(filename, ext))
continue;
Replay *rpy = malloc(sizeof(Replay));
if(!replay_load(rpy, e->d_name, REPLAY_READ_META)) {
if(!replay_load(rpy, filename, REPLAY_READ_META)) {
free(rpy);
continue;
}
@ -283,14 +281,13 @@ int fill_replayview_menu(MenuData *m) {
memset(ictx, 0, sizeof(ReplayviewItemContext));
ictx->replay = rpy;
ictx->replayname = malloc(strlen(e->d_name) + 1);
strcpy(ictx->replayname, e->d_name);
ictx->replayname = strdup(filename);
add_menu_entry(m, " ", replayview_run, ictx)->transition = rpy->numstages < 2 ? TransFadeBlack : NULL;
++rpys;
}
closedir(dir);
vfs_dir_close(dir);
if(m->entries) {
qsort(m->entries, m->ecount, sizeof(MenuEntry), replayview_cmp);

View file

@ -1,18 +0,0 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (C) 2011, Lukas Weber <laochailan@web.de>
*/
#ifndef NATIVE_H
#define NATIVE_H
const char *get_prefix(void);
const char *get_config_path(void);
const char *get_screenshots_path(void);
const char *get_replays_path(void);
void init_paths(void);
#endif

View file

@ -1,27 +0,0 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (C) 2011, Lukas Weber <laochailan@web.de>
*/
#include "native.h"
const char *get_prefix(void) {
return "./data/";
}
const char *get_config_path(void) {
return ".";
}
const char *get_screenshots_path(void) {
return "./screenshots";
}
const char *get_replays_path(void) {
return "./replays";
}
void init_paths(void) {
}

View file

@ -1,51 +0,0 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (C) 2011, Lukas Weber <laochailan@web.de>
*/
#include "native.h"
#include "global.h"
#include <stdlib.h>
#include <string.h>
#define DATA_DIR "data/"
#define SCR_DIR "screenshots"
#define RPY_DIR "replays"
char *content_path;
char *conf_path;
char *scr_path;
char *rpy_path;
const char *get_prefix(void) {
return content_path;
}
const char *get_config_path(void) {
return conf_path;
}
const char *get_screenshots_path(void) {
return scr_path;
}
const char *get_replays_path(void) {
return rpy_path;
}
void init_paths(void) {
#ifdef RELATIVE
char *basedir = SDL_GetBasePath();
content_path = strjoin(basedir, DATA_DIR, NULL);
free(basedir);
#else
content_path = FILE_PREFIX;
#endif
conf_path = SDL_GetPrefPath("", "taisei");
scr_path = strjoin(conf_path, SCR_DIR, NULL);
rpy_path = strjoin(conf_path, RPY_DIR, NULL);
}

View file

@ -1,41 +0,0 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (C) 2011, Lukas Weber <laochailan@web.de>
*/
#include "native.h"
#include "global.h"
#include <stdlib.h>
#include <string.h>
#define CFG_DIR "/.taisei"
#define SCR_DIR "/screenshots"
#define RPY_DIR "/replays"
char *conf_path;
char *scr_path;
char *rpy_path;
const char *get_prefix(void) {
return FILE_PREFIX;
}
const char *get_config_path(void) {
return conf_path;
}
const char *get_screenshots_path(void) {
return scr_path;
}
const char *get_replays_path(void) {
return rpy_path;
}
void init_paths(void) {
conf_path = strjoin(getenv("HOME"), CFG_DIR, NULL);
scr_path = strjoin(conf_path, SCR_DIR, NULL);
rpy_path = strjoin(conf_path, RPY_DIR, NULL);
}

View file

@ -8,7 +8,6 @@
#include <zlib.h>
#include "progress.h"
#include "paths/native.h"
#include "stage.h"
/*
@ -68,10 +67,6 @@ static uint8_t progress_magic_bytes[] = {
0x00, 0x67, 0x74, 0x66, 0x6f, 0xe3, 0x83, 0x84
};
static char* progress_getpath(void) {
return strfmt("%s/%s", get_config_path(), PROGRESS_FILENAME);
}
static uint32_t progress_checksum(uint8_t *buf, size_t num) {
return crc32(0xB16B00B5, buf, num);
}
@ -573,31 +568,25 @@ void progress_load(void) {
progress_save();
#endif
char *p = progress_getpath();
SDL_RWops *file = SDL_RWFromFile(p, "rb");
SDL_RWops *file = vfs_open(PROGRESS_FILE, VFS_MODE_READ);
if(!file) {
log_warn("Couldn't open the progress file: %s", SDL_GetError());
free(p);
log_warn("Couldn't open the progress file: %s", vfs_get_error());
return;
}
free(p);
progress_read(file);
SDL_RWclose(file);
}
void progress_save(void) {
char *p = progress_getpath();
SDL_RWops *file = SDL_RWFromFile(p, "wb");
SDL_RWops *file = vfs_open(PROGRESS_FILE, VFS_MODE_WRITE);
if(!file) {
log_warn("Couldn't open the progress file: %s", SDL_GetError());
free(p);
log_warn("Couldn't open the progress file: %s", vfs_get_error());
return;
}
free(p);
progress_write(file);
SDL_RWclose(file);
}

View file

@ -12,7 +12,7 @@
#include <SDL.h>
#include "ending.h"
#define PROGRESS_FILENAME "progress.dat"
#define PROGRESS_FILE "storage/progress.dat"
#define PROGRESS_MAXFILESIZE 4096
#ifdef DEBUG

View file

@ -14,7 +14,6 @@
#include <time.h>
#include "global.h"
#include "paths/native.h"
static uint8_t replay_magic_header[] = REPLAY_MAGIC_HEADER;
@ -137,7 +136,7 @@ static void replay_write_string(SDL_RWops *file, char *str) {
SDL_RWwrite(file, str, 1, strlen(str));
}
static int replay_write_stage_event(ReplayEvent *evt, SDL_RWops *file) {
static bool replay_write_stage_event(ReplayEvent *evt, SDL_RWops *file) {
SDL_WriteLE32(file, evt->frame);
SDL_WriteU8(file, evt->type);
SDL_WriteLE16(file, evt->value);
@ -166,7 +165,7 @@ static uint32_t replay_calc_stageinfo_checksum(ReplayStage *stg) {
return cs;
}
static int replay_write_stage(ReplayStage *stg, SDL_RWops *file) {
static bool replay_write_stage(ReplayStage *stg, SDL_RWops *file) {
SDL_WriteLE16(file, stg->stage);
SDL_WriteLE32(file, stg->seed);
SDL_WriteU8(file, stg->diff);
@ -188,7 +187,7 @@ static int replay_write_stage(ReplayStage *stg, SDL_RWops *file) {
return true;
}
int replay_write(Replay *rpy, SDL_RWops *file, bool compression) {
bool replay_write(Replay *rpy, SDL_RWops *file, bool compression) {
uint16_t version = REPLAY_STRUCT_VERSION;
int i, j;
@ -271,7 +270,7 @@ static void replay_read_string(SDL_RWops *file, char **ptr) {
SDL_RWread(file, *ptr, 1, len);
}
static int replay_read_header(Replay *rpy, SDL_RWops *file, int64_t filesize, size_t *ofs) {
static bool replay_read_header(Replay *rpy, SDL_RWops *file, int64_t filesize, size_t *ofs) {
uint8_t header[sizeof(replay_magic_header)];
(*ofs) += sizeof(header);
@ -298,7 +297,7 @@ static int replay_read_header(Replay *rpy, SDL_RWops *file, int64_t filesize, si
return true;
}
static int replay_read_meta(Replay *rpy, SDL_RWops *file, int64_t filesize) {
static bool replay_read_meta(Replay *rpy, SDL_RWops *file, int64_t filesize) {
replay_read_string(file, &rpy->playername);
PRINTPROP(rpy->playername, s);
@ -341,7 +340,7 @@ static int replay_read_meta(Replay *rpy, SDL_RWops *file, int64_t filesize) {
return true;
}
static int replay_read_events(Replay *rpy, SDL_RWops *file, int64_t filesize) {
static bool replay_read_events(Replay *rpy, SDL_RWops *file, int64_t filesize) {
for(int i = 0; i < rpy->numstages; ++i) {
ReplayStage *stg = rpy->stages + i;
@ -365,8 +364,8 @@ static int replay_read_events(Replay *rpy, SDL_RWops *file, int64_t filesize) {
return true;
}
int replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode) {
int64_t filesize; // must be signed
bool replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode) {
ssize_t filesize; // must be signed
SDL_RWops *vfile = file;
mode &= REPLAY_READ_ALL;
@ -478,58 +477,73 @@ int replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode) {
#undef CHECKPROP
#undef PRINTPROP
char* replay_getpath(const char *name, bool ext) {
return ext ? strfmt("%s/%s.%s", get_replays_path(), name, REPLAY_EXTENSION) :
strfmt("%s/%s", get_replays_path(), name);
static char* replay_getpath(const char *name, bool ext) {
return ext ? strfmt("storage/replays/%s.%s", name, REPLAY_EXTENSION) :
strfmt("storage/replays/%s", name);
}
int replay_save(Replay *rpy, const char *name) {
bool replay_save(Replay *rpy, const char *name) {
char *p = replay_getpath(name, !strendswith(name, REPLAY_EXTENSION));
log_info("Saving %s", p);
char *sp = vfs_syspath_or_repr(p);
log_info("Saving %s", sp);
free(sp);
SDL_RWops *file = SDL_RWFromFile(p, "wb");
SDL_RWops *file = vfs_open(p, VFS_MODE_WRITE);
free(p);
if(!file) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
log_warn("VFS error: %s", vfs_get_error());
return false;
}
int result = replay_write(rpy, file, REPLAY_WRITE_COMPRESSED);
bool result = replay_write(rpy, file, REPLAY_WRITE_COMPRESSED);
SDL_RWclose(file);
return result;
}
int replay_load(Replay *rpy, const char *name, ReplayReadMode mode) {
char *p;
bool replay_load(Replay *rpy, const char *name, ReplayReadMode mode) {
char *p = replay_getpath(name, !strendswith(name, REPLAY_EXTENSION));
char *sp = vfs_syspath_or_repr(p);
log_info("Loading %s (mode %i)", sp, mode);
free(sp);
if(mode & REPLAY_READ_RAWPATH) {
p = (char*)name;
} else {
p = replay_getpath(name, !strendswith(name, REPLAY_EXTENSION));
SDL_RWops *file = vfs_open(p, VFS_MODE_READ);
free(p);
if(!file) {
log_warn("VFS error: %s", vfs_get_error());
return false;
}
log_info("replay_load(): loading %s (mode %i)", p, mode);
bool result = replay_read(rpy, file, mode);
if(!result) {
replay_destroy(rpy);
}
SDL_RWclose(file);
return result;
}
bool replay_load_syspath(Replay *rpy, const char *path, ReplayReadMode mode) {
log_info("Loading %s (mode %i)", path, mode);
SDL_RWops *file;
#ifndef __WINDOWS__
if(!strcmp(name,"-"))
if(!strcmp(path, "-"))
file = SDL_RWFromFP(stdin,false);
else
file = SDL_RWFromFile(p, "rb");
file = SDL_RWFromFile(path, "rb");
#else
file = SDL_RWFromFile(p, "rb");
file = SDL_RWFromFile(path, "rb");
#endif
if(!(mode & REPLAY_READ_RAWPATH)) {
free(p);
}
if(!file) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
return false;
}
int result = replay_read(rpy, file, mode);
bool result = replay_read(rpy, file, mode);
if(!result) {
replay_destroy(rpy);
@ -690,13 +704,3 @@ void replay_play(Replay *rpy, int firstidx) {
global.replay_stage = NULL;
free_resources(false);
}
void replay_play_path(const char *path, int firstidx) {
replay_destroy(&global.replay);
if(!replay_load(&global.replay, path, REPLAY_READ_ALL | REPLAY_READ_RAWPATH)) {
return;
}
replay_play(&global.replay, firstidx);
}

View file

@ -139,7 +139,6 @@ typedef enum ReplayReadMode {
REPLAY_READ_META = 1,
REPLAY_READ_EVENTS = 2,
REPLAY_READ_ALL = 3, // includes the other two
REPLAY_READ_RAWPATH = 4, // only used by replay_load()
} ReplayReadMode;
void replay_init(Replay *rpy);
@ -152,18 +151,16 @@ void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, int16_t
void replay_stage_check_desync(ReplayStage *stg, int time, uint16_t check, ReplayMode mode);
void replay_stage_sync_player_state(ReplayStage *stg, Player *plr);
int replay_write(Replay *rpy, SDL_RWops *file, bool compression);
int replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode);
bool replay_write(Replay *rpy, SDL_RWops *file, bool compression);
bool replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode);
int replay_save(Replay *rpy, const char *name);
int replay_load(Replay *rpy, const char *name, ReplayReadMode mode);
bool replay_save(Replay *rpy, const char *name);
bool replay_load(Replay *rpy, const char *name, ReplayReadMode mode);
bool replay_load_syspath(Replay *rpy, const char *path, ReplayReadMode mode);
void replay_copy(Replay *dst, Replay *src, bool steal_events);
char* replay_getpath(const char *name, bool ext); // must be freed
void replay_play(Replay *rpy, int firstidx);
void replay_play_path(const char *path, int firstidx);
int replay_find_stage_idx(Replay *rpy, uint8_t stageid);

View file

@ -21,6 +21,6 @@ void* load_music_begin(const char *path, unsigned int flags);
void* load_music_end(void *opaque, const char *path, unsigned int flags);
void unload_music(void *snd);
#define BGM_PATH_PREFIX "bgm/"
#define BGM_PATH_PREFIX "res/bgm/"
#endif

View file

@ -23,7 +23,14 @@ bool check_music_path(const char *path) {
}
void* load_music_begin(const char *path, unsigned int flags) {
Mix_Music *music = Mix_LoadMUS(path);
SDL_RWops *rwops = vfs_open(path, VFS_MODE_READ);
if(!rwops) {
log_warn("VFS error: %s", vfs_get_error());
return NULL;
}
Mix_Music *music = Mix_LoadMUS_RW(rwops, true);
if(!music) {
log_warn("Mix_LoadMUS() failed: %s", Mix_GetError());

View file

@ -7,25 +7,30 @@
#include "font.h"
#include "global.h"
#include "paths/native.h"
struct Fonts _fonts;
TTF_Font* load_font(char *name, int size) {
char *buf = strjoin(get_prefix(), name, NULL);
TTF_Font* load_font(char *vfspath, int size) {
char *syspath = vfs_syspath_or_repr(vfspath);
SDL_RWops *rwops = vfs_open(vfspath, VFS_MODE_READ);
if(!rwops) {
log_fatal("VFS error: %s", vfs_get_error());
}
// XXX: what would be the best rounding strategy here?
size = rint(size * resources.fontren.quality);
TTF_Font *f = TTF_OpenFont(buf, size);
TTF_Font *f = TTF_OpenFontRW(rwops, true, size);
if(!f) {
log_fatal("Failed to load font '%s' @ %i: %s", buf, size, TTF_GetError());
log_fatal("Failed to load font '%s' @ %i: %s", syspath, size, TTF_GetError());
}
log_info("Loaded '%s' @ %i", buf, size);
log_info("Loaded '%s' @ %i", syspath, size);
free(buf);
free(syspath);
return f;
}
@ -125,9 +130,9 @@ void uninit_fonts(void) {
void load_fonts(float quality) {
fontrenderer_init(&resources.fontren, quality);
_fonts.standard = load_font("gfx/LinBiolinum.ttf", 20);
_fonts.mainmenu = load_font("gfx/immortal.ttf", 35);
_fonts.small = load_font("gfx/LinBiolinum.ttf", 14);
_fonts.standard = load_font("res/gfx/LinBiolinum.ttf", 20);
_fonts.mainmenu = load_font("res/gfx/immortal.ttf", 35);
_fonts.small = load_font("res/gfx/LinBiolinum.ttf", 14);
}
void reload_fonts(float quality) {

View file

@ -131,10 +131,10 @@ static void free_obj(ObjFileData *data) {
}
static void parse_obj(const char *filename, ObjFileData *data) {
SDL_RWops *rw = SDL_RWFromFile(filename, "r");
SDL_RWops *rw = vfs_open(filename, VFS_MODE_READ);
if(!rw) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
log_warn("VFS error: %s", vfs_get_error());
return;
}

View file

@ -46,7 +46,7 @@ Model* get_model(const char *name);
void draw_model_p(Model *model);
void draw_model(const char *name);
#define MDL_PATH_PREFIX "models/"
#define MDL_PATH_PREFIX "res/models/"
#define MDL_EXTENSION ".obj"
#endif

View file

@ -6,9 +6,6 @@
*/
#include "resource.h"
#include <sys/types.h>
#include <sys/stat.h>
#include "paths/native.h"
#include "config.h"
#include "video.h"
#include "menu/mainmenu.h"
@ -274,7 +271,9 @@ static Resource* load_resource_finish(void *opaque, ResourceHandler *handler, co
return NULL;
}
Resource *res = insert_resource(handler->type, name, raw, flags, path);
char *sp = vfs_syspath_or_repr(path);
Resource *res = insert_resource(handler->type, name, raw, flags, sp);
free(sp);
free(allocated_path);
free(allocated_name);
@ -387,11 +386,11 @@ const char* resource_util_filename(const char *path) {
void load_resources(void) {
if(glext.draw_instanced) {
load_shader_snippets("shader/laser_snippets", "laser_", RESF_PERMANENT);
load_shader_snippets(SHA_PATH_PREFIX "laser_snippets", "laser_", RESF_PERMANENT);
}
menu_preload();
resources.stage_postprocess = postprocess_load("shader/postprocess.conf");
resources.stage_postprocess = postprocess_load(SHA_PATH_PREFIX "postprocess.conf");
}
void free_resources(bool all) {
@ -408,9 +407,9 @@ void free_resources(bool all) {
if(!all && res->flags & RESF_PERMANENT)
continue;
ResourceFlags flags = res->flags;
ResourceFlags flags __attribute__((unused)) = res->flags;
unload_resource(res);
log_info("Unloaded %s '%s' (%s)", resource_type_names[type], name,
log_debug("Unloaded %s '%s' (%s)", resource_type_names[type], name,
(flags & RESF_PERMANENT) ? "permanent" : "transient"
);

View file

@ -19,7 +19,6 @@
#include "model.h"
#include "postprocess.h"
#include "hashtable.h"
#include "paths/native.h"
typedef enum ResourceType {
RES_TEXTURE,
@ -39,21 +38,18 @@ typedef enum ResourceFlags {
#define RESF_DEFAULT 0
// All paths are relative to the current working directory, which can assumed to be the resources directory,
// unless mentioned otherwise.
// Converts a path into an abstract resource name to be used as the hashtable key.
// Converts a vfs path into an abstract resource name to be used as the hashtable key.
// This method is optional, the default strategy is to take the path minus the prefix and extension.
// The returned name must be free()'d.
typedef char* (*ResourceNameFunc)(const char *path);
// Converts an abstract resource name into a path from which a resource with that name could be loaded.
// Converts an abstract resource name into a vfs path from which a resource with that name could be loaded.
// The path may not actually exist or be usable. The load function (see below) shall deal with such cases.
// The returned path must be free()'d.
// May return NULL on failure, but does not have to.
typedef char* (*ResourceFindFunc)(const char *name);
// Tells whether the resource handler should attempt to load a file, specified by a path.
// Tells whether the resource handler should attempt to load a file, specified by a vfs path.
typedef bool (*ResourceCheckFunc)(const char *path);
// Begins loading a resource specified by path.

View file

@ -21,6 +21,6 @@ void* load_sound_begin(const char *path, unsigned int flags);
void* load_sound_end(void *opaque, const char *path, unsigned int flags);
void unload_sound(void *snd);
#define SFX_PATH_PREFIX "sfx/"
#define SFX_PATH_PREFIX "res/sfx/"
#endif

View file

@ -23,7 +23,14 @@ bool check_sound_path(const char *path) {
}
void* load_sound_begin(const char *path, unsigned int flags) {
Mix_Chunk *sound = Mix_LoadWAV(path);
SDL_RWops *rwops = vfs_open(path, VFS_MODE_READ);
if(!rwops) {
log_warn("VFS error: %s", vfs_get_error());
return NULL;
}
Mix_Chunk *sound = Mix_LoadWAV_RW(rwops, true);
if(!sound) {
log_warn("Mix_LoadWAV() failed: %s", Mix_GetError());

View file

@ -30,7 +30,7 @@ Shader* get_shader_optional(const char *name);
int uniloc(Shader *sha, const char *name);
#define SHA_PATH_PREFIX "shader/"
#define SHA_PATH_PREFIX "res/shader/"
#define SHA_EXTENSION ".sha"
#define SHA_DELIM "%% -- FRAG"

View file

@ -10,7 +10,6 @@
#include "texture.h"
#include "resource.h"
#include "global.h"
#include "paths/native.h"
#include "vbo.h"
char* texture_path(const char *name) {
@ -86,10 +85,6 @@ Texture* prefix_get_tex(const char *name, const char *prefix) {
static ImageData* load_png_p(const char *filename, SDL_RWops *rwops) {
#define PNGFAIL(msg) { log_warn("Couldn't load '%s': %s", filename, msg); return NULL; }
if(!rwops) {
PNGFAIL(SDL_GetError())
}
png_structp png_ptr;
if(!(png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL))) {
PNGFAIL("png_create_read_struct() failed")
@ -150,13 +145,16 @@ static ImageData* load_png_p(const char *filename, SDL_RWops *rwops) {
}
static ImageData* load_png(const char *filename) {
SDL_RWops *rwops = SDL_RWFromFile(filename, "r");
ImageData *img = load_png_p(filename, rwops);
SDL_RWops *rwops = vfs_open(filename, VFS_MODE_READ);
if(rwops) {
SDL_RWclose(rwops);
if(!rwops) {
log_warn("VFS error: %s", vfs_get_error());
return NULL;
}
ImageData *img = load_png_p(filename, rwops);
SDL_RWclose(rwops);
return img;
}

View file

@ -41,7 +41,7 @@ void loop_tex_line(complex a, complex b, float w, float t, const char *texture);
Texture* get_tex(const char *name);
Texture* prefix_get_tex(const char *name, const char *prefix);
#define TEX_PATH_PREFIX "gfx/"
#define TEX_PATH_PREFIX "res/gfx/"
#define TEX_EXTENSION ".png"
#endif

View file

@ -129,6 +129,16 @@ void strip_trailing_slashes(char *buf) {
*c = 0;
}
char* strappend(char **dst, char *src) {
if(!*dst) {
return *dst = strdup(src);
}
*dst = realloc(*dst, strlen(*dst) + strlen(src) + 1);
strcat(*dst, src);
return *dst;
}
/*
* public domain strtok_r() by Charlie Gordon
*
@ -310,10 +320,10 @@ char* read_all(const char *filename, int *outsize) {
char *text;
size_t size;
SDL_RWops *file = SDL_RWFromFile(filename, "r");
SDL_RWops *file = vfs_open(filename, VFS_MODE_READ);
if(!file) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
log_warn("VFS error: %s", vfs_get_error());
return NULL;
}
@ -388,10 +398,10 @@ bool parse_keyvalue_stream_cb(SDL_RWops *strm, KVCallback callback, void *data)
}
bool parse_keyvalue_file_cb(const char *filename, KVCallback callback, void *data) {
SDL_RWops *strm = SDL_RWFromFile(filename, "r");
SDL_RWops *strm = vfs_open(filename, VFS_MODE_READ);
if(!strm) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
log_warn("VFS error: %s", vfs_get_error());
return false;
}

View file

@ -11,6 +11,7 @@
#include <SDL.h>
#include "log.h"
#include "hashtable.h"
#include "vfs/public.h"
//
// compatibility
@ -39,6 +40,7 @@ char* vstrfmt(const char *fmt, va_list args);
char* strfmt(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void strip_trailing_slashes(char *buf);
char* strtok_r(char *str, const char *delim, char **nextp);
char* strappend(char **dst, char *src);
#undef strdup
#define strdup SDL_strdup
@ -135,19 +137,13 @@ noreturn void _ts_assert_fail(const char *cond, const char *func, const char *fi
#define assert(cond) _assert(cond, true)
#define assert_nolog(cond) _assert(cond, false)
#ifndef __POSIX__
#define MKDIR(p) mkdir(p)
#else
#define MKDIR(p) mkdir(p, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
#endif
//
// safeguards against some dangerous or otherwise undesirable practices
//
#undef fopen
FILE* fopen() __attribute__((deprecated(
"Use SDL_RWFromFile instead")));
"Use vfs_open or SDL_RWFromFile instead")));
#undef strncat
char* strncat() __attribute__((deprecated(

95
src/vfs/pathutil.c Normal file
View file

@ -0,0 +1,95 @@
#include "private.h"
char* vfs_path_normalize(const char *path, char *out) {
const char *p = path;
char *o = out;
char *last_sep = out - 1;
char *path_end = strchr(path, 0);
while(p < path_end) {
if(strchr(VFS_PATH_SEPS, *p)) {
if(o > out && *(o - 1) != VFS_PATH_SEP) {
last_sep = o;
*o++ = VFS_PATH_SEP;
}
do {
++p;
} while(strchr(VFS_PATH_SEPS, *p));
} else if(*p == '.' && strchr(VFS_PATH_SEPS, *(p + 1))) {
p += 2;
} else if(!strncmp(p, "..", 2) && strchr(VFS_PATH_SEPS, *(p + 2))) {
if(last_sep >= out) {
do {
--last_sep;
} while(*last_sep != VFS_PATH_SEP && last_sep >= out);
o = last_sep-- + 1;
}
p += 3;
} else {
*o++ = *p++;
}
}
*o = 0;
// log_debug("[%s] --> [%s]", path, out);
return out;
}
char* vfs_path_normalize_alloc(const char *path) {
return vfs_path_normalize(path, strdup(path));
}
char* vfs_normalize_path_inplace(char *path) {
char buf[strlen(path)+1];
strcpy(buf, path);
vfs_path_normalize(path, buf);
strcpy(path, buf);
return path;
}
void vfs_path_split_left(char *path, char **lpath, char **rpath) {
char *sep;
while(*path == VFS_PATH_SEP)
++path;
*lpath = path;
if(sep = strchr(path, VFS_PATH_SEP)) {
*sep = 0;
*rpath = sep + 1;
} else {
*rpath = path + strlen(path);
}
}
void vfs_path_split_right(char *path, char **lpath, char **rpath) {
char *sep, *c;
assert(*path != 0);
while(*(c = strrchr(path, 0) - 1) == VFS_PATH_SEP)
*c = 0;
if(sep = strrchr(path, VFS_PATH_SEP)) {
*sep = 0;
*lpath = path;
*rpath = sep + 1;
} else {
*lpath = path + strlen(path);
*rpath = path;
}
}
void vfs_path_root_prefix(char *path) {
if(!strchr(VFS_PATH_SEPS, *path)) {
memmove(path+1, path, strlen(path)+1);
*path = VFS_PATH_SEP;
} else {
*path = VFS_PATH_SEP;
}
}

22
src/vfs/pathutil.h Normal file
View file

@ -0,0 +1,22 @@
#ifndef TAISEI_VFS_PATHUTIL
#define TAISEI_VFS_PATHUTIL
#include <SDL_platform.h>
#ifdef __WINDOWS__
#define VFS_PATH_SEPS "/\\"
#else
#define VFS_PATH_SEPS "/"
#endif
#define VFS_PATH_SEP VFS_PATH_SEPS[0]
char* vfs_path_normalize(const char *path, char *out);
char* vfs_path_normalize_alloc(const char *path);
char* vfs_path_normalize_inplace(char *path);
void vfs_path_split_left(char *path, char **lpath, char **rpath);
void vfs_path_split_right(char *path, char **lpath, char **rpath);
void vfs_path_root_prefix(char *path);
#endif

228
src/vfs/private.c Normal file
View file

@ -0,0 +1,228 @@
#include "private.h"
#include "vdir.h"
VFSNode *vfs_root;
typedef struct vfs_tls_s {
char *error_str;
} vfs_tls_t;
static SDL_TLSID vfs_tls_id;
static vfs_tls_t *vfs_tls_fallback;
static void vfs_tls_free(void *vtls) {
if(vtls) {
vfs_tls_t *tls = vtls;
free(tls->error_str);
free(tls);
}
}
static vfs_tls_t* vfs_tls_get(void) {
if(vfs_tls_id) {
return SDL_TLSGet(vfs_tls_id);
}
return vfs_tls_fallback;
}
void vfs_init(void) {
vfs_root = vfs_alloc();
vfs_vdir_init(vfs_root);
vfs_root->name = strdup("");
vfs_tls_id = SDL_TLSCreate();
if(vfs_tls_id) {
SDL_TLSSet(vfs_tls_id, calloc(1, sizeof(vfs_tls_t)), vfs_tls_free);
vfs_tls_fallback = NULL;
} else {
log_warn("SDL_TLSCreate(): failed: %s", SDL_GetError());
vfs_tls_fallback = calloc(1, sizeof(vfs_tls_t));
}
}
void vfs_uninit(void) {
vfs_free(vfs_root);
vfs_tls_free(vfs_tls_fallback);
vfs_root = NULL;
vfs_tls_id = 0;
vfs_tls_fallback = NULL;
}
VFSNode* vfs_alloc(void) {
return calloc(1, sizeof(VFSNode));
}
void vfs_free(VFSNode *node) {
if(!node) {
return;
}
if(node->funcs && node->funcs->free) {
node->funcs->free(node);
}
free(node->name);
free(node);
}
VFSInfo vfs_query_node(VFSNode *node) {
assert(node != NULL);
assert(node->funcs != NULL);
assert(node->funcs->query != NULL);
return node->funcs->query(node);
}
VFSNode* vfs_locate(VFSNode *root, const char *path) {
assert(root != NULL);
assert(path != NULL);
assert(root->funcs != NULL);
#ifndef NDEBUG
char buf[strlen(path)+1];
strcpy(buf, path);
assert(!strcmp(path, vfs_path_normalize(path, buf)));
#endif
if(!*path) {
return root;
}
if(root->funcs->locate) {
return root->funcs->locate(root, path);
}
return NULL;
}
void vfs_locate_cleanup(VFSNode *root, VFSNode *node) {
if(node && !node->parent && node != root) {
vfs_free(node);
}
}
bool vfs_mount(VFSNode *root, const char *mountpoint, VFSNode *subtree) {
VFSNode *mpnode;
char buf[2][strlen(mountpoint)+1];
char *mpbase, *mpname;
bool result = false;
mountpoint = vfs_path_normalize(mountpoint, buf[0]);
strcpy(buf[1], buf[0]);
vfs_path_split_right(buf[1], &mpbase, &mpname);
if(mpnode = vfs_locate(root, mountpoint)) {
// mountpoint already exists - try to merge with the target node
if(mpnode->funcs->mount) {
// expected to set error on failure
result = mpnode->funcs->mount(mpnode, NULL, subtree);
} else {
result = false;
vfs_set_error("Mountpoint '%s' already exists and does not support merging", mountpoint);
}
vfs_locate_cleanup(root, mpnode);
return result;
}
if(mpnode = vfs_locate(root, mpbase)) {
// try to become a subnode of parent (conventional mount)
if(mpnode->funcs->mount) {
// expected to set error on failure
result = mpnode->funcs->mount(mpnode, mpname, subtree);
} else {
result = false;
vfs_set_error("Parent directory '%s' of mountpoint '%s' does not support mounting", mpbase, mountpoint);
}
vfs_locate_cleanup(root, mpnode);
}
return result;
}
const char* vfs_iter(VFSNode *node, void **opaque) {
if(node->funcs->iter) {
return node->funcs->iter(node, opaque);
}
return NULL;
}
void vfs_iter_stop(VFSNode *node, void **opaque) {
if(node->funcs->iter_stop) {
node->funcs->iter_stop(node, opaque);
}
}
VFSNode* vfs_find_root(VFSNode *node) {
VFSNode *root = node, *r = node;
while(r = r->parent)
root = r;
return root;
}
char* vfs_repr(VFSNode *node) {
assert(node != NULL);
assert(node->funcs != NULL);
assert(node->funcs->repr != NULL);
VFSInfo i = vfs_query_node(node);
char *r, *o = node->funcs->repr(node);
r = strfmt("<%s (t:%i e:%i x:%i d:%i)>", o,
node->type, i.error, i.exists, i.is_dir
);
free(o);
return r;
}
void vfs_print_tree_recurse(SDL_RWops *dest, VFSNode *root, char *prefix) {
void *o = NULL;
char *newprefix = strfmt("%s%s%s", prefix, root->name, vfs_query_node(root).is_dir ? (char[]){VFS_PATH_SEP, 0} : "");
char *r;
SDL_RWprintf(dest, "%s = %s\n", newprefix, r = vfs_repr(root));
free(r);
for(const char *n; n = vfs_iter(root, &o);) {
VFSNode *node = vfs_locate(root, n);
if(node) {
vfs_print_tree_recurse(dest, node, newprefix);
vfs_locate_cleanup(root, node);
}
}
vfs_iter_stop(root, &o);
free(newprefix);
}
const char* vfs_get_error(void) {
vfs_tls_t *tls = vfs_tls_get();
return tls->error_str ? tls->error_str : "No error";
}
void vfs_set_error(char *fmt, ...) {
vfs_tls_t *tls = vfs_tls_get();
va_list args;
va_start(args, fmt);
char *err = vstrfmt(fmt, args);
free(tls->error_str);
tls->error_str = err;
va_end(args);
log_debug("%s", tls->error_str);
}
void vfs_set_error_from_sdl(void) {
vfs_set_error("SDL error: %s", SDL_GetError());
}

92
src/vfs/private.h Normal file
View file

@ -0,0 +1,92 @@
#ifndef TAISEI_VFS_PRIVATE
#define TAISEI_VFS_PRIVATE
/*
* This file should not be included by code outside of the vfs/ directory
*/
#include "util.h"
#include "public.h"
#include "pathutil.h"
typedef struct VFSNode VFSNode;
typedef enum VFSNodeType {
VNODE_VDIR,
VNODE_SYSPATH,
VNODE_UNION,
} VFSNodeType;
typedef char* (*VFSReprFunc)(VFSNode*);
typedef void (*VFSFreeFunc)(VFSNode*);
typedef VFSInfo (*VFSQueryFunc)(VFSNode*);
typedef bool (*VFSMountFunc)(VFSNode *mountroot, const char *subname, VFSNode *mountee);
typedef char* (*VFSSysPathFunc)(VFSNode*);
typedef VFSNode* (*VFSLocateFunc)(VFSNode *dirnode, const char* path);
typedef const char* (*VFSIterFunc)(VFSNode *dirnode, void **opaque);
typedef void (*VFSIterStopFunc)(VFSNode *dirnode, void **opaque);
typedef bool (*VFSMkDirFunc)(VFSNode *parent, const char *subdir);
typedef SDL_RWops* (*VFSOpenFunc)(VFSNode *filenode, VFSOpenMode mode);
typedef struct VFSNodeFuncs {
VFSReprFunc repr;
VFSFreeFunc free;
VFSQueryFunc query;
VFSMountFunc mount;
VFSSysPathFunc syspath;
VFSLocateFunc locate;
VFSIterFunc iter;
VFSIterStopFunc iter_stop;
VFSMkDirFunc mkdir;
VFSOpenFunc open;
} VFSNodeFuncs;
typedef struct VFSNode {
VFSNodeType type;
VFSNode *parent;
VFSNodeFuncs *funcs;
char *name;
union {
// TODO: maybe somehow separate this
struct {
Hashtable *contents;
} vdir;
struct {
char *path;
} syspath;
struct {
struct {
ListContainer *all;
VFSNode *primary;
} members;
} vunion;
};
} VFSNode;
extern VFSNode *vfs_root;
VFSNode* vfs_alloc(void);
void vfs_free(VFSNode *node);
char* vfs_repr(VFSNode *node);
VFSNode* vfs_locate(VFSNode *root, const char *path);
void vfs_locate_cleanup(VFSNode *root, VFSNode *node);
VFSInfo vfs_query_node(VFSNode *node);
bool vfs_mount(VFSNode *root, const char *mountpoint, VFSNode *subtree);
const char* vfs_iter(VFSNode *node, void **opaque);
void vfs_iter_stop(VFSNode *node, void **opaque);
VFSNode* vfs_find_root(VFSNode *node);
void vfs_set_error(char *fmt, ...) __attribute__((format(printf, 1, 2)));
void vfs_set_error_from_sdl(void);
void vfs_print_tree_recurse(SDL_RWops *dest, VFSNode *root, char *prefix);
#endif

217
src/vfs/public.c Normal file
View file

@ -0,0 +1,217 @@
#include "private.h"
#include "union.h"
#include "syspath.h"
typedef struct VFSDir {
VFSNode *node;
void *opaque;
} VFSDir;
bool vfs_create_union_mountpoint(const char *mountpoint) {
VFSNode *unode = vfs_alloc();
vfs_union_init(unode);
if(!vfs_mount(vfs_root, mountpoint, unode)) {
vfs_free(unode);
return false;
}
return true;
}
bool vfs_mount_syspath(const char *mountpoint, const char *fspath, bool mkdir) {
VFSNode *rdir = vfs_alloc();
vfs_syspath_init(rdir, fspath);
assert(rdir->funcs);
assert(rdir->funcs->mkdir);
if(mkdir && !rdir->funcs->mkdir(rdir, NULL)) {
vfs_set_error("Can't create directory: %s", vfs_get_error());
return false;
}
if(!vfs_mount(vfs_root, mountpoint, rdir)) {
vfs_free(rdir);
return false;
}
return true;
}
SDL_RWops* vfs_open(const char *path, VFSOpenMode mode) {
SDL_RWops *rwops = NULL;
char p[strlen(path)+1];
path = vfs_path_normalize(path, p);
VFSNode *node = vfs_locate(vfs_root, path);
if(node) {
assert(node->funcs != NULL);
if(node->funcs->open) {
// expected to set error on failure
rwops = node->funcs->open(node, mode);
vfs_locate_cleanup(vfs_root, node);
} else {
vfs_set_error("Node '%s' can't be opened as a file", path);
}
} else {
vfs_set_error("Node '%s' does not exist", path);
}
return rwops;
}
VFSInfo vfs_query(const char *path) {
char p[strlen(path)+1];
path = vfs_path_normalize(path, p);
VFSNode *node = vfs_locate(vfs_root, path);
if(node) {
// expected to set error on failure
// not that e.g. a file not existing on a real filesystem
// is not an error condition. If we can't tell whether it
// exists or not, that is an error.
VFSInfo i = vfs_query_node(node);
vfs_locate_cleanup(vfs_root, node);
return i;
}
vfs_set_error("Node '%s' does not exist", path);
return VFSINFO_ERROR;
}
bool vfs_mkdir(const char *path) {
char p[strlen(path)+1];
path = vfs_path_normalize(path, p);
VFSNode *node = vfs_locate(vfs_root, path);
if(node && node->funcs->mkdir) {
return node->funcs->mkdir(node, NULL);
}
char *parent, *subdir;
vfs_path_split_right(p, &parent, &subdir);
node = vfs_locate(vfs_root, parent);
if(node) {
if(node->funcs->mkdir) {
return node->funcs->mkdir(node, subdir);
} else {
vfs_set_error("Node '%s' does not support creation of subdirectories", parent);
}
} else {
vfs_set_error("Node '%s' does not exist", parent);
}
return false;
}
void vfs_mkdir_required(const char *path) {
if(!vfs_mkdir(path)) {
log_fatal("%s", vfs_get_error());
}
}
static char* vfs_syspath_node(VFSNode *node, const char *path) {
assert(node->funcs);
char *p = NULL;
if(node->funcs->syspath) {
// expected to set error on failure
p = node->funcs->syspath(node);
} else {
vfs_set_error("Node '%s' does not represent a real filesystem path", path);
}
return p;
}
static char* vfs_syspath_internal(const char *path, bool repr) {
char p[strlen(path)+1];
path = vfs_path_normalize(path, p);
VFSNode *node = vfs_locate(vfs_root, path);
if(node) {
char *p = vfs_syspath_node(node, path);
if(!p && repr) {
p = vfs_repr(node);
}
return p;
}
vfs_set_error("Node '%s' does not exist", path);
return NULL;
}
char* vfs_syspath(const char *path) {
return vfs_syspath_internal(path, false);
}
char* vfs_syspath_or_repr(const char *path) {
return vfs_syspath_internal(path, true);
}
bool vfs_print_tree(SDL_RWops *dest, char *path) {
char p[strlen(path)+3], *ignore;
path = vfs_path_normalize(path, p);
VFSNode *node = vfs_locate(vfs_root, path);
if(!node) {
vfs_set_error("Node '%s' does not exist", path);
return false;
}
if(*path) {
vfs_path_split_right(path, &path, &ignore);
if(*path) {
char *e = strchr(path, 0);
*e++ = '/';
*e = 0;
}
vfs_path_root_prefix(path);
}
vfs_print_tree_recurse(dest, node, path);
return true;
}
VFSDir* vfs_dir_open(const char *path) {
char p[strlen(path)+1];
path = vfs_path_normalize(path, p);
VFSNode *node = vfs_locate(vfs_root, path);
if(node) {
if(node->funcs->iter && vfs_query_node(node).is_dir) {
VFSDir *d = calloc(1, sizeof(VFSDir));
d->node = node;
return d;
} else {
vfs_set_error("Node '%s' is not a directory", path);
}
vfs_locate_cleanup(vfs_root, node);
} else {
vfs_set_error("Node '%s' does not exist", path);
}
return NULL;
}
void vfs_dir_close(VFSDir *dir) {
if(dir) {
vfs_iter_stop(dir->node, &dir->opaque);
vfs_locate_cleanup(vfs_root, dir->node);
}
}
const char* vfs_dir_read(VFSDir *dir) {
if(dir) {
return vfs_iter(dir->node, &dir->opaque);
}
return NULL;
}

44
src/vfs/public.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef TAISEI_VFS_PUBLIC
#define TAISEI_VFS_PUBLIC
#include <SDL.h>
#include <stdbool.h>
typedef struct VFSInfo {
unsigned int error: 1;
unsigned int exists : 1;
unsigned int is_dir : 1;
} VFSInfo;
#define VFSINFO_ERROR ((VFSInfo){.error = true, 0})
typedef enum VFSOpenMode {
VFS_MODE_READ = 1,
VFS_MODE_WRITE = 2,
} VFSOpenMode;
typedef struct VFSDir VFSDir;
SDL_RWops* vfs_open(const char *path, VFSOpenMode mode);
VFSInfo vfs_query(const char *path);
bool vfs_mkdir(const char *path);
void vfs_mkdir_required(const char *path);
bool vfs_create_union_mountpoint(const char *mountpoint);
bool vfs_mount_syspath(const char *mountpoint, const char *fspath, bool mkdir);
VFSDir* vfs_dir_open(const char *path);
void vfs_dir_close(VFSDir *dir);
const char* vfs_dir_read(VFSDir *dir);
char* vfs_syspath(const char *path);
char* vfs_syspath_or_repr(const char *path);
bool vfs_print_tree(SDL_RWops *dest, char *path);
// these are defined in private.c, but need to be accessible from external code
void vfs_init(void);
void vfs_uninit(void);
const char* vfs_get_error(void);
#endif

9
src/vfs/syspath.h Normal file
View file

@ -0,0 +1,9 @@
#ifndef TAISEI_VFS_SYSPATH
#define TAISEI_VFS_SYSPATH
#include "private.h"
void vfs_syspath_init(VFSNode *node, const char *path);
#endif

143
src/vfs/syspath_posix.c Normal file
View file

@ -0,0 +1,143 @@
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include "syspath.h"
static void vfs_syspath_init_internal(VFSNode *node, char *path, char *name);
static void vfs_syspath_free(VFSNode *node) {
free(node->syspath.path);
}
static VFSInfo vfs_syspath_query(VFSNode *node) {
struct stat fstat;
VFSInfo i = {0};
if(stat(node->syspath.path, &fstat) >= 0) {
i.exists = true;
i.is_dir = S_ISDIR(fstat.st_mode);
}
return i;
}
static SDL_RWops* vfs_syspath_open(VFSNode *node, VFSOpenMode mode) {
SDL_RWops *rwops = SDL_RWFromFile(node->syspath.path, mode == VFS_MODE_WRITE ? "w" : "r");
if(!rwops) {
vfs_set_error_from_sdl();
}
return rwops;
}
static VFSNode* vfs_syspath_locate(VFSNode *node, const char *path) {
VFSNode *n = vfs_alloc();
char buf[strlen(path)+1], *base, *name;
strcpy(buf, path);
vfs_path_split_right(buf, &base, &name);
vfs_syspath_init_internal(n, strfmt("%s%c%s", node->syspath.path, VFS_PATH_SEP, path), strdup(name));
return n;
}
static const char* vfs_syspath_iter(VFSNode *node, void **opaque) {
DIR *dir;
struct dirent *e;
if(!*opaque) {
*opaque = opendir(node->syspath.path);
}
if(!(dir = *opaque)) {
return NULL;
}
do {
e = readdir(dir);
} while(e && (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")));
if(e) {
return e->d_name;
}
closedir(dir);
*opaque = NULL;
return NULL;
}
static void vfs_syspath_iter_stop(VFSNode *node, void **opaque) {
if(*opaque) {
closedir((DIR*)*opaque);
*opaque = NULL;
}
}
static char* vfs_syspath_repr(VFSNode *node) {
return strfmt("filesystem path (posix): %s", node->syspath.path);
}
static char* vfs_syspath_syspath(VFSNode *node) {
return strdup(node->syspath.path);
}
// TODO: get rid of this when the win32 version of this module is implemented
#ifndef __POSIX__
#define mkdir(p,m) mkdir(p)
#else
#define mkdir(p,m) mkdir(p, m)
#endif
static bool vfs_syspath_mkdir(VFSNode *node, const char *subdir) {
if(!subdir) {
subdir = "";
}
char *p = strfmt("%s%c%s", node->syspath.path, VFS_PATH_SEP, subdir);
bool ok = !mkdir(p, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
if(!ok && errno == EEXIST) {
VFSNode *n = vfs_locate(node, subdir);
if(n && vfs_query_node(n).is_dir) {
ok = true;
}
if(node != n) {
vfs_locate_cleanup(vfs_root, n);
}
}
if(!ok) {
vfs_set_error("Can't create directory %s (errno: %i)", p, errno);
}
free(p);
return ok;
}
static VFSNodeFuncs vfs_funcs_syspath = {
.repr = vfs_syspath_repr,
.query = vfs_syspath_query,
.free = vfs_syspath_free,
.locate = vfs_syspath_locate,
.syspath = vfs_syspath_syspath,
.iter = vfs_syspath_iter,
.iter_stop = vfs_syspath_iter_stop,
.mkdir = vfs_syspath_mkdir,
.open = vfs_syspath_open,
};
static void vfs_syspath_init_internal(VFSNode *node, char *path, char *name) {
node->type = VNODE_SYSPATH;
node->funcs = &vfs_funcs_syspath;
node->syspath.path = path;
node->name = name;
}
void vfs_syspath_init(VFSNode *node, const char *path) {
vfs_syspath_init_internal(node, strdup(path), NULL);
}

2
src/vfs/syspath_win32.c Normal file
View file

@ -0,0 +1,2 @@
#error Not yet implemented

248
src/vfs/union.c Normal file
View file

@ -0,0 +1,248 @@
#include "union.h"
static bool vfs_union_mount_internal(VFSNode *unode, const char *mountpoint, VFSNode *mountee, VFSInfo info, bool seterror);
static void vfs_union_delete_callback(void **list, void *elem) {
ListContainer *c = elem;
VFSNode *n = c->data;
vfs_free(n);
delete_element(list, elem);
}
static void vfs_union_free(VFSNode *node) {
delete_all_elements((void**)&node->vunion.members, vfs_union_delete_callback);
}
static VFSNode* vfs_union_locate(VFSNode *node, const char *path) {
VFSNode *u = vfs_alloc();
vfs_union_init(u); // uniception!
VFSInfo prim_info = VFSINFO_ERROR;
ListContainer *first = node->vunion.members.all;
ListContainer *last = first;
ListContainer *c;
for(c = first; c; c = c->next) {
last = c;
}
for(c = last; c; c = c->prev) {
VFSNode *n = c->data;
VFSNode *o = vfs_locate(n, path);
if(o) {
VFSInfo i = vfs_query_node(o);
if(vfs_union_mount_internal(u, NULL, o, i, false)) {
prim_info = i;
}
}
}
if(u->vunion.members.primary) {
if(!u->vunion.members.all->next || !prim_info.is_dir) {
// the temporary union contains just one member, or doesn't represent a directory
// in those cases it's just a useless wrapper, so let's just return the primary member directly
VFSNode *n = u->vunion.members.primary;
// remove the primary member from the 'all' list to prevent vfs_free from wrecking it
delete_element((void**)&u->vunion.members.all, u->vunion.members.all);
vfs_free(u);
return n;
}
u->name = strdup(u->vunion.members.primary->name);
} else {
// all in vain...
vfs_free(u);
u = NULL;
}
return u;
}
typedef struct VFSUnionIterData {
Hashtable *visited;
ListContainer *current;
void *opaque;
} VFSUnionIterData;
static const char* vfs_union_iter(VFSNode *node, void **opaque) {
VFSUnionIterData *i = *opaque;
const char *r = NULL;
if(!i) {
i = malloc(sizeof(VFSUnionIterData));
i->current = node->vunion.members.all;
i->opaque = NULL;
// XXX: this may not be the most efficient implementation of a "set" structure...
i->visited = hashtable_new_stringkeys(37);
*opaque = i;
}
while(i->current) {
VFSNode *n = (VFSNode*)i->current->data;
r = vfs_iter(n, &i->opaque);
if(!r) {
vfs_iter_stop(n, &i->opaque);
i->opaque = NULL;
i->current = i->current->next;
continue;
}
if(hashtable_get_string(i->visited, r)) {
continue;
}
hashtable_set_string(i->visited, r, (void*)true);
break;
}
return r;
}
static void vfs_union_iter_stop(VFSNode *node, void **opaque) {
VFSUnionIterData *i = *opaque;
if(i) {
hashtable_free(i->visited);
free(i);
}
*opaque = NULL;
}
static VFSInfo vfs_union_query(VFSNode *node) {
if(node->vunion.members.primary) {
return vfs_query_node(node->vunion.members.primary);
}
vfs_set_error("Union object has no members");
return VFSINFO_ERROR;
}
static bool vfs_union_mount_internal(VFSNode *unode, const char *mountpoint, VFSNode *mountee, VFSInfo info, bool seterror) {
if(!info.exists) {
if(seterror) {
vfs_set_error("Mountee doesn't represent a usable resource");
} else {
vfs_free(mountee);
}
return false;
}
if(seterror && !info.is_dir) {
vfs_set_error("Mountee is not a directory");
return false;
}
ListContainer *c = NULL, *p = unode->vunion.members.all;
create_container(&c)->data = mountee;
c->next = p;
if(p) {
p->prev = c;
}
unode->vunion.members.all = c;
unode->vunion.members.primary = mountee;
return true;
}
static bool vfs_union_mount(VFSNode *unode, const char *mountpoint, VFSNode *mountee) {
if(mountpoint) {
vfs_set_error("Attempted to use a named mountpoint on a union");
return false;
}
return vfs_union_mount_internal(unode, NULL, mountee, vfs_query_node(mountee), true);
}
static SDL_RWops* vfs_union_open(VFSNode *unode, VFSOpenMode mode) {
VFSNode *n = unode->vunion.members.primary;
if(n) {
if(n->funcs->open) {
return n->funcs->open(n, mode);
} else {
vfs_set_error("Primary union member can't be opened as a file");
}
} else {
vfs_set_error("Union object has no members");
}
return NULL;
}
static char* vfs_union_repr(VFSNode *node) {
char *mlist = strdup("union: "), *r;
for(ListContainer *c = node->vunion.members.all; c; c = c->next) {
VFSNode *n = c->data;
strappend(&mlist, r = vfs_repr(n));
free(r);
if(c->next) {
strappend(&mlist, ", ");
}
}
return mlist;
}
static char* vfs_union_syspath(VFSNode *node) {
VFSNode *n = node->vunion.members.primary;
if(n) {
if(n->funcs->syspath) {
return n->funcs->syspath(node);
} else {
vfs_set_error("Primary union member doesn't represent a real filesystem path");
}
}
vfs_set_error("Union object has no members");
return NULL;
}
static bool vfs_union_mkdir(VFSNode *node, const char *subdir) {
VFSNode *n = node->vunion.members.primary;
if(n) {
if(n->funcs->mkdir) {
return n->funcs->mkdir(node, subdir);
} else {
vfs_set_error("Primary union member doesn't support directory creation");
}
}
vfs_set_error("Union object has no members");
return false;
}
static VFSNodeFuncs vfs_funcs_union = {
.repr = vfs_union_repr,
.query = vfs_union_query,
.free = vfs_union_free,
.syspath = vfs_union_syspath,
.mount = vfs_union_mount,
.locate = vfs_union_locate,
.iter = vfs_union_iter,
.iter_stop = vfs_union_iter_stop,
.mkdir = vfs_union_mkdir,
.open = vfs_union_open,
};
void vfs_union_init(VFSNode *node) {
node->type = VNODE_UNION;
node->funcs = &vfs_funcs_union;
}

9
src/vfs/union.h Normal file
View file

@ -0,0 +1,9 @@
#ifndef TAISEI_VFS_UNION
#define TAISEI_VFS_UNION
#include "private.h"
void vfs_union_init(VFSNode *node);
#endif

117
src/vfs/vdir.c Normal file
View file

@ -0,0 +1,117 @@
#include "vdir.h"
static void vfs_vdir_detach_node(VFSNode *vdir, VFSNode *node, bool unset) {
assert(node->parent == vdir);
if(unset) {
hashtable_unset_string(vdir->vdir.contents, node->name);
}
free(node->name);
node->name = NULL;
}
static void vfs_vdir_attach_node(VFSNode *vdir, const char *name, VFSNode *node) {
assert(node->parent == NULL);
assert(node->name == NULL);
VFSNode *oldnode = hashtable_get_string(vdir->vdir.contents, name);
if(oldnode) {
vfs_vdir_detach_node(vdir, oldnode, true);
}
node->name = strdup(name);
node->parent = vdir;
hashtable_set_string(vdir->vdir.contents, name, node);
}
static VFSNode* vfs_vdir_locate(VFSNode *vdir, const char *path) {
VFSNode *node;
char mutpath[strlen(path)+1];
char *primpath, *subpath;
assert(vdir->type == VNODE_VDIR);
strcpy(mutpath, path);
vfs_path_split_left(mutpath, &primpath, &subpath);
if(node = hashtable_get_string(vdir->vdir.contents, mutpath)) {
return vfs_locate(node, subpath);
}
return NULL;
}
static const char* vfs_vdir_iter(VFSNode *vdir, void **opaque) {
char *ret = NULL;
if(!*opaque) {
*opaque = hashtable_iter(vdir->vdir.contents);
}
if(!hashtable_iter_next((HashtableIterator*)*opaque, (void**)&ret, NULL)) {
*opaque = NULL;
}
return ret;
}
static void vfs_vdir_iter_stop(VFSNode *vdir, void **opaque) {
if(*opaque) {
free(*opaque);
*opaque = NULL;
}
}
static VFSInfo vfs_vdir_query(VFSNode *vdir) {
return (VFSInfo) {
.exists = true,
.is_dir = true,
};
}
static void vfs_vdir_free(VFSNode *vdir) {
Hashtable *ht = vdir->vdir.contents;
HashtableIterator *i;
VFSNode *child;
for(i = hashtable_iter(ht); hashtable_iter_next(i, NULL, (void**)&child);) {
vfs_vdir_detach_node(vdir, child, false);
vfs_free(child);
}
hashtable_free(ht);
}
static bool vfs_vdir_mount(VFSNode *vdir, const char *mountpoint, VFSNode *subtree) {
if(!mountpoint) {
// merge attempt, unsupported
return false;
}
vfs_vdir_attach_node(vdir, mountpoint, subtree);
return true;
}
static char* vfs_vdir_repr(VFSNode *node) {
return strdup("virtual directory");
}
static VFSNodeFuncs vfs_funcs_vdir = {
.repr = vfs_vdir_repr,
.query = vfs_vdir_query,
.free = vfs_vdir_free,
.mount = vfs_vdir_mount,
.locate = vfs_vdir_locate,
.iter = vfs_vdir_iter,
.iter_stop = vfs_vdir_iter_stop,
};
void vfs_vdir_init(VFSNode *node) {
node->type = VNODE_VDIR;
node->funcs = &vfs_funcs_vdir;
node->vdir.contents = hashtable_new_stringkeys(HT_DYNAMIC_SIZE);
}

9
src/vfs/vdir.h Normal file
View file

@ -0,0 +1,9 @@
#ifndef TAISEI_VFS_VDIR
#define TAISEI_VFS_VDIR
#include "private.h"
void vfs_vdir_init(VFSNode *node);
#endif

View file

@ -265,7 +265,7 @@ void video_setmode(int w, int h, bool fs, bool resizable) {
void video_take_screenshot(void) {
SDL_RWops *out;
char *data;
char outfile[128], *outpath;
char outfile[128], *outpath, *syspath;
time_t rawtime;
struct tm * timeinfo;
int w, h, rw, rh;
@ -283,15 +283,18 @@ void video_take_screenshot(void) {
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(outfile, 128, "/taisei_%Y%m%d_%H-%M-%S_%Z.png", timeinfo);
strftime(outfile, 128, "taisei_%Y%m%d_%H-%M-%S_%Z.png", timeinfo);
outpath = strjoin(get_screenshots_path(), outfile, NULL);
log_info("Saving screenshot as %s", outpath);
out = SDL_RWFromFile(outpath, "w");
outpath = strjoin("storage/screenshots/", outfile, NULL);
syspath = vfs_syspath_or_repr(outpath);
log_info("Saving screenshot as %s", syspath);
free(syspath);
out = vfs_open(outpath, VFS_MODE_WRITE);
free(outpath);
if(!out) {
log_warn("SDL_RWFromFile() failed: %s", SDL_GetError());
log_warn("VFS error: %s", vfs_get_error());
free(data);
return;
}