658 lines
14 KiB
C++
658 lines
14 KiB
C++
#include <std_include.hpp>
|
|
#include <loader/module_loader.hpp>
|
|
#include "game/game.hpp"
|
|
|
|
#include <utils/hook.hpp>
|
|
|
|
#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<const char**>(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 <directory> [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 <filter>\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 <file>\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<void>(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<void>(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<char*>(buffer);
|
|
auto remaining = len;
|
|
auto tries = 0;
|
|
while (remaining)
|
|
{
|
|
const auto block = remaining;
|
|
const auto written = static_cast<int>(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<const game::native::dvar_t**>(SELECT_VALUE(0x1C2B538, 0x59ADD18));
|
|
fs_debug = reinterpret_cast<const game::native::dvar_t**>(SELECT_VALUE(0x1C2B32C, 0x59A9A08));
|
|
fs_ignoreLocalized = reinterpret_cast<const game::native::dvar_t**>(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<const char*>(SELECT_VALUE(0x629031, 0x5B0FD1), "userraw");
|
|
}
|
|
|
|
REGISTER_MODULE(file_system)
|