#include #include #include "game/game.hpp" #include #include "command.hpp" #include "console.hpp" #include "file_system.hpp" namespace { utils::hook::detour sys_default_install_path_hook; const game::native::dvar_t** fs_homepath; const game::native::dvar_t** fs_debug; const game::native::dvar_t** fs_ignoreLocalized; FILE* file_for_handle(const int f) { assert(!game::native::fsh[f].zipFile); assert(game::native::fsh[f].handleFiles.file.o); return game::native::fsh[f].handleFiles.file.o; } unsigned int file_write(const void* ptr, const unsigned int len, FILE* stream) { return std::fwrite(ptr, sizeof(char), len, stream); } FILE* file_open_append_text(const char* filename) { errno = 0; auto* file = std::fopen(filename, "at"); if (file) { return file; } console::error("Couldn't open file: %s %s\n", filename, std::strerror(errno)); return nullptr; } FILE* file_open_write_binary(const char* filename) { errno = 0; auto* file = std::fopen(filename, "wb"); if (file) { return file; } console::error("Couldn't open file: %s %s\n", filename, std::strerror(errno)); return nullptr; } void replace_separators(char* path) { char* src, * dst; bool was_sep = false; for (src = path, dst = path; *src; ++src) { if (*src == '/' || *src == '\\') { if (!was_sep) { was_sep = true; *dst++ = '\\'; } } else { was_sep = false; *dst++ = *src; } } *dst = 0; } void build_os_path_for_thread(const char* base, const char* game, const char* qpath, char* ospath, game::native::FsThread thread) { assert(base); assert(qpath); assert(ospath); if (!game) { game = ""; } else if (!game[0]) { game = game::native::fs_gamedir; } auto len_base = std::strlen(base); auto len_game = std::strlen(game); auto len_qpath = std::strlen(qpath); if (len_game + 1 + len_base + len_qpath + 1 >= game::native::MAX_OSPATH) { if (thread) { *ospath = '\0'; return; } game::native::Com_Error(game::native::ERR_FATAL, "\x15" "FS_BuildOSPath: os path length exceeded\n"); } std::memcpy(ospath, base, len_base); ospath[len_base] = '/'; std::memcpy(&ospath[len_base + 1], game, len_game); ospath[len_base + 1 + len_game] = '/'; std::memcpy(ospath + len_base + 2 + len_game, qpath, len_qpath + 1); replace_separators(ospath); } game::native::FsThread get_current_thread() { if (game::native::Sys_IsMainThread()) { return game::native::FS_THREAD_MAIN; } if (game::native::Sys_IsDatabaseThread()) { return game::native::FS_THREAD_DATABASE; } if (game::native::Sys_IsStreamThread()) { return game::native::FS_THREAD_STREAM; } if (game::native::Sys_IsRenderThread()) { return game::native::FS_THREAD_BACKEND; } if (game::native::Sys_IsServerThread()) { return game::native::FS_THREAD_SERVER; } return game::native::FS_THREAD_INVALID; } int handle_for_file_current_thread() { return game::native::FS_HandleForFile(get_current_thread()); } int open_file_append(const char* filename) { char ospath[game::native::MAX_OSPATH]{}; game::native::FS_CheckFileSystemStarted(); const auto* basepath = (*fs_homepath)->current.string; build_os_path_for_thread(basepath, game::native::fs_gamedir, filename, ospath, game::native::FS_THREAD_MAIN); if ((*fs_debug)->current.integer) { console::info("FS_FOpenFileAppend: %s\n", ospath); } if (game::native::FS_CreatePath(ospath)) { return 0; } auto* f = file_open_append_text(ospath); if (!f) { return 0; } auto h = handle_for_file_current_thread(); game::native::fsh[h].zipFile = nullptr; strncpy_s(game::native::fsh[h].name, filename, _TRUNCATE); game::native::fsh[h].handleFiles.file.o = f; game::native::fsh[h].handleSync = 0; if (!game::native::fsh[h].handleFiles.file.o) { game::native::FS_FCloseFile(h); h = 0; } return h; } int get_handle_and_open_file(const char* filename, const char* ospath, game::native::FsThread thread) { auto* fp = file_open_write_binary(ospath); if (!fp) { return 0; } const auto f = game::native::FS_HandleForFile(thread); game::native::fsh[f].zipFile = nullptr; game::native::fsh[f].handleFiles.file.o = fp; strncpy_s(game::native::fsh[f].name, filename, _TRUNCATE); game::native::fsh[f].handleSync = 0; return f; } int open_file_write_to_dir_for_thread(const char* filename, const char* dir, const char* osbasepath, game::native::FsThread thread) { char ospath[game::native::MAX_OSPATH]{}; game::native::FS_CheckFileSystemStarted(); const char* basepath = (*fs_homepath)->current.string; build_os_path_for_thread(basepath, dir, filename, ospath, game::native::FS_THREAD_MAIN); if ((*fs_debug)->current.integer) { console::info("FS_FOpenFileWriteToDirForThread: %s\n", ospath); } if (game::native::FS_CreatePath(ospath)) { return 0; } return get_handle_and_open_file(filename, ospath, thread); } int open_file_write(const char* filename) { return open_file_write_to_dir_for_thread(filename, game::native::fs_gamedir, "", game::native::FS_THREAD_MAIN); } void convert_path(char* s) { while (*s) { if (*s == '\\' || *s == ':') { *s = '/'; } ++s; } } int path_cmp(const char* s1, const char* s2) { int c1; do { c1 = *s1++; int c2 = *s2++; if (game::native::I_islower(c1)) { c1 -= ('a' - 'A'); } if (game::native::I_islower(c2)) { c2 -= ('a' - 'A'); } if (c1 == '\\' || c1 == ':') { c1 = '/'; } if (c2 == '\\' || c2 == ':') { c2 = '/'; } if (c1 < c2) { return -1; // strings not equal } if (c1 > c2) { return 1; } } while (c1); return 0; // strings are equal } void sort_file_list(char** filelist, int numfiles) { int j; const char** sortedlist = static_cast(game::native::Z_Malloc((numfiles * sizeof(*sortedlist)) + 4)); sortedlist[0] = nullptr; auto numsortedfiles = 0; for (auto i = 0; i < numfiles; ++i) { for (j = 0; j < numsortedfiles; j++) { if (path_cmp(filelist[i], sortedlist[j]) < 0) { break; } } for (auto k = numsortedfiles; k > j; --k) { sortedlist[k] = sortedlist[k - 1]; } sortedlist[j] = filelist[i]; ++numsortedfiles; } std::memcpy(filelist, sortedlist, numfiles * sizeof(*filelist)); std::free(sortedlist); } int use_search_path(game::native::searchpath_s* pSearch) { if (pSearch->bLocalized && (*fs_ignoreLocalized)->current.enabled) { return 0; } if (pSearch->bLocalized && pSearch->language != game::native::SEH_GetCurrentLanguage()) { return 0; } return 1; } int iwd_is_pure(game::native::iwd_t* iwd) { if (*game::native::fs_numServerIwds) { for (auto i = 0; i < *game::native::fs_numServerIwds; ++i) { if (iwd->checksum == game::native::fs_serverIwds[i]) { return 1; } } return 0; } return 1; } void display_path(bool b_language_cull) { auto i_language = game::native::SEH_GetCurrentLanguage(); const auto* psz_language_name = game::native::SEH_GetLanguageName(i_language); console::info("Current language: %s\n", psz_language_name); if ((*fs_ignoreLocalized)->current.enabled) { console::info(" localized assets are being ignored\n"); } console::info("Current search path:\n"); for (auto* s = *game::native::fs_searchpaths; s; s = s->next) { if (b_language_cull && !use_search_path(s)) { continue; } if (s->iwd) { console::info("%s (%i files)\n", s->iwd->iwdFilename, s->iwd->numfiles); if (s->bLocalized) { console::info(" localized assets iwd file for %s\n", game::native::SEH_GetLanguageName(s->language)); } if (*game::native::fs_numServerIwds) { if (iwd_is_pure(s->iwd)) { console::info(" on the pure list\n"); } else { console::info(" not on the pure list\n"); } } } else { console::info("%s/%s\n", s->dir->path, s->dir->gamedir); if (s->bLocalized) { console::info(" localized assets game folder for %s\n", game::native::SEH_GetLanguageName(s->language)); } } } console::info("\nFile Handles:\n"); for (int i = 1; i < 64; ++i) { if (game::native::fsh[i].handleFiles.file.o) { console::info("handle %i: %s\n", i, game::native::fsh[i].name); } } } bool touch_file(const char* name) { *game::native::com_fileAccessed = 1; auto ret = game::native::FS_FOpenFileReadForThread(name, nullptr, game::native::FS_THREAD_MAIN); return ret != -1; } void path_f() { display_path(true); } void full_path_f() { display_path(false); } void dir_f() { const char* path; const char* extension; int ndirs; if (game::native::Cmd_Argc() < 2 || game::native::Cmd_Argc() > 3) { console::info("usage: dir [extension]\n"); return; } if (game::native::Cmd_Argc() == 2) { path = game::native::Cmd_Argv(1); extension = ""; } else { path = game::native::Cmd_Argv(1); extension = game::native::Cmd_Argv(2); } console::info("Directory of %s %s\n", path, extension); console::info("---------------\n"); auto** dirnames = file_system::list_files(path, extension, game::native::FS_LIST_PURE_ONLY, &ndirs, 3); for (int i = 0; i < ndirs; ++i) { console::info("%s\n", dirnames[i]); } game::native::Sys_FreeFileList(dirnames); } void new_dir_f() { int ndirs; if (game::native::Cmd_Argc() < 2) { console::info("usage: fdir \n"); console::info("example: fdir *q3dm*.bsp\n"); return; } const auto* filter = game::native::Cmd_Argv(1); console::info("---------------\n"); auto** dirnames = game::native::FS_ListFilteredFiles(*game::native::fs_searchpaths, "", "", filter, game::native::FS_LIST_PURE_ONLY, &ndirs, 3); sort_file_list(dirnames, ndirs); for (auto i = 0; i < ndirs; ++i) { convert_path(dirnames[i]); console::info("%s\n", dirnames[i]); } console::info("%d files listed\n", ndirs); game::native::Sys_FreeFileList(dirnames); } void touch_file_f() { if (game::native::Cmd_Argc() != 2) { console::info("Usage: touchFile \n"); return; } touch_file(game::native::Cmd_Argv(1)); } void add_commands() { Cmd_AddCommand("path", path_f); Cmd_AddCommand("fullpath", full_path_f); Cmd_AddCommand("dir", dir_f); Cmd_AddCommand("fdir", new_dir_f); Cmd_AddCommand("touchFile", touch_file_f); } void fs_startup_stub(char* game_name) { console::info("----- FS_Startup -----\n"); utils::hook::invoke(0x5B1070, game_name); add_commands(); display_path(true); console::info("----------------------\n"); console::info("%d files in iwd files\n", *game::native::fs_iwdFileCount); } void fs_shutdown_stub(int closemfp) { utils::hook::invoke(0x5B0D30, closemfp); game::native::Cmd_RemoveCommand("path"); game::native::Cmd_RemoveCommand("fullpath"); game::native::Cmd_RemoveCommand("dir"); game::native::Cmd_RemoveCommand("fdir"); game::native::Cmd_RemoveCommand("touchFile"); } const char* sys_default_install_path_stub() { static auto current_path = std::filesystem::current_path().string(); return current_path.data(); } } int file_system::open_file_by_mode(const char* qpath, int* f, game::native::fsMode_t mode) { auto r = 6969; auto sync = 0; switch (mode) { case game::native::FS_READ: *game::native::com_fileAccessed = TRUE; r = game::native::FS_FOpenFileReadForThread(qpath, f, game::native::FS_THREAD_MAIN); break; case game::native::FS_WRITE: *f = open_file_write(qpath); r = 0; if (!*f) { r = -1; } break; case game::native::FS_APPEND_SYNC: sync = 1; case game::native::FS_APPEND: *f = open_file_append(qpath); r = 0; if (!*f ) { r = -1; } break; default: game::native::Com_Error(game::native::ERR_FATAL, "\x15" "FSH_FOpenFile: bad mode"); break; } if (!f) { return r; } if (*f) { game::native::fsh[*f].fileSize = r; game::native::fsh[*f].streamed = 0; } game::native::fsh[*f].handleSync = sync; return r; } int file_system::write(const char* buffer, int len, int h) { game::native::FS_CheckFileSystemStarted(); if (!h) { return 0; } auto* f = file_for_handle(h); auto* buf = const_cast(buffer); auto remaining = len; auto tries = 0; while (remaining) { const auto block = remaining; const auto written = static_cast(file_write(buf, block, f)); if (!written) { if (tries) { return 0; } tries = 1; } if (written == -1) { return 0; } remaining -= written; buf += written; } if (game::native::fsh[h].handleSync) { std::fflush(f); } return len; } char** file_system::list_files(const char* path, const char* extension, game::native::FsListBehavior_e behavior, int* numfiles, int allocTrackType) { return game::native::FS_ListFilteredFiles(*game::native::fs_searchpaths, path, extension, nullptr, behavior, numfiles, allocTrackType); } void file_system::post_load() { fs_homepath = reinterpret_cast(SELECT_VALUE(0x1C2B538, 0x59ADD18)); fs_debug = reinterpret_cast(SELECT_VALUE(0x1C2B32C, 0x59A9A08)); fs_ignoreLocalized = reinterpret_cast(SELECT_VALUE(0x1C2B21C, 0x59A99F8)); if (game::is_mp()) { utils::hook(0x5B20AA, fs_startup_stub, HOOK_CALL).install()->quick(); // FS_InitFilesystem utils::hook(0x5B2148, fs_startup_stub, HOOK_CALL).install()->quick(); // FS_Restart utils::hook(0x5557CC, fs_shutdown_stub, HOOK_CALL).install()->quick(); // Com_Quit_f utils::hook(0x5B2115, fs_shutdown_stub, HOOK_CALL).install()->quick(); // FS_Restart } // Make open-iw5 work outside of the game directory sys_default_install_path_hook.create(SELECT_VALUE(0x487E50, 0x5C4A80), &sys_default_install_path_stub); // fs_basegame utils::hook::set(SELECT_VALUE(0x629031, 0x5B0FD1), "userraw"); } REGISTER_MODULE(file_system)