From 7aa6bd1cd88db9cceef3d1c07cd7664cb47538be Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 12 Jun 2023 22:28:53 +0100 Subject: Remove the terrible gameinfo.txt garbage at last This also tidies up library handle grabbing with more os.h stuff, and improves the VDF creation logic - since we no longer store a couple of paths which makes it necessary to change that a bit anyway. --- src/engineapi.c | 1 - src/gameinfo.c | 276 ++++---------------------------------------------------- src/gameinfo.h | 13 +-- src/l4dmm.c | 8 +- src/os-unix.h | 9 ++ src/os-win32.h | 7 ++ src/sst.c | 115 ++++++++++++++--------- 7 files changed, 109 insertions(+), 320 deletions(-) diff --git a/src/engineapi.c b/src/engineapi.c index 831836b..f4a54d6 100644 --- a/src/engineapi.c +++ b/src/engineapi.c @@ -71,7 +71,6 @@ bool engineapi_init(int pluginver) { void *pim = factory_server("PlayerInfoManager002", 0); if (pim) globalvars = GetGlobalVars(pim); - inputsystem = factory_inputsystem("InputSystemVersion001", 0); vgui = factory_engine("VEngineVGui001", 0); void *srvdll; diff --git a/src/gameinfo.c b/src/gameinfo.c index 71922a6..9a6686e 100644 --- a/src/gameinfo.c +++ b/src/gameinfo.c @@ -14,226 +14,25 @@ * PERFORMANCE OF THIS SOFTWARE. */ -#ifdef _WIN32 -#include -#endif - -#include "con_.h" #include "engineapi.h" #include "errmsg.h" #include "gamedata.h" #include "gametype.h" -#include "intdefs.h" -#include "kv.h" #include "os.h" #include "vcall.h" -#ifdef _WIN32 -#define fS "S" // os string (wide string) to regular string -#define Fs L"S" // regular string to os string (wide string) -#define PATHSEP L"\\" // for joining. could just be / but \ is more consistent -#else -// everything is just a regular string already -#define fS "s" -#define Fs "s" -#define PATHSEP "/" -#endif - -// ~~TODO(opt): get rid of the rest of the snprintf and strcpy, some day~~ -// TODO(opt): remove almost all this parsing nonsense, it's not needed any more! -// We can simply GetWindowText (and do a little more work on Linux...) and do -// away with absolute paths to DLLs which won't be required with deferred init. - -static os_char bindir[PATH_MAX] = {0}; #ifdef _WIN32 static os_char gamedir[PATH_MAX] = {0}; #endif -static os_char clientlib[PATH_MAX] = {0}; -static os_char serverlib[PATH_MAX] = {0}; static char title[64] = {0}; -const os_char *gameinfo_bindir = bindir; const os_char *gameinfo_gamedir #ifdef _WIN32 = gamedir // on linux, the pointer gets directly set in gameinfo_init() #endif ; -const os_char *gameinfo_clientlib = clientlib; -const os_char *gameinfo_serverlib = serverlib; const char *gameinfo_title = title; -// case insensitive substring match, expects s2 to be lowercase already! -// note: in theory this shouldn't need to be case sensitive, but I've seen mods -// use both lowercase and TitleCase so this is just to be as lenient as possible -static bool matchtok(const char *s1, const char *s2, usize sz) { - for (; sz; --sz, ++s1, ++s2) if (tolower(*s1) != *s2) return false; - return true; -} - -static void trygamelib(const os_char *path, os_char *outpath) { - // _technically_ this is toctou, but I don't think that matters here - if (os_access(path, F_OK) != -1) { - os_strcpy(outpath, path); - } - else if (errno != ENOENT) { - errmsg_warnstd("failed to access %" fS, path); - } -} - -// note: p and len are a non-null-terminated string -static inline void dolibsearch(const char *p, uint len, bool isgamebin, - const os_char *cwd) { - // sanity check: don't do a bunch of work for no reason - if (len >= PATH_MAX - 1 - (sizeof("client" OS_DLSUFFIX) - 1)) goto toobig; - os_char bindir[PATH_MAX]; - os_char *outp = bindir; - // this should really be an snprintf, meh whatever - os_strcpy(bindir, cwd); - outp = bindir + os_strlen(bindir); - // quick note about windows encoding conversion: this MIGHT clobber the - // encoding of non-ascii mod names, but it's unclear if/how source handles - // that anyway, so we just have to assume there *are no* non-ascii mod - // names, since they'd also be clobbered, probably. NOTE that this - // assumption does NOT apply to the absolute base path; see further down. - const os_char *fmt = isgamebin ? - OS_LIT("/%.*") Fs OS_LIT("/") : - OS_LIT("/%.*") Fs OS_LIT("/bin/"); - int spaceleft = PATH_MAX; - if (len >= 25 && matchtok(p, "|all_source_engine_paths|", 25)) { - // this special path doesn't seem any different to normal, - // why is this a thing? - p += 25; len -= 25; - } - else if (len >= 15 && matchtok(p, "|gameinfo_path|", 15)) { - // search in the actual mod/game directory - p += 15; len -= 15; - int ret = os_snprintf(bindir, PATH_MAX, OS_LIT("%s"), gamedir); - outp = bindir + ret; - spaceleft -= ret; - } - else { -#ifdef _WIN32 - // sigh - char api_needs_null_term[PATH_MAX]; - memcpy(api_needs_null_term, p, len * sizeof(*p)); - api_needs_null_term[len] = L'\0'; - if (!PathIsRelativeA(api_needs_null_term)) -#else - if (*p == '/') // so much easier :') -#endif - { - // the mod path is absolute, so we're not sticking anything else in - // front of it, so skip the leading slash in fmt and point the pointer - // at the start of the buffer - ++fmt; - outp = bindir; - }} - - // leave room for server/client.dll/so (note: server and client happen to - // conveniently have the same number of letters) - int fmtspace = spaceleft - (sizeof("client" OS_DLSUFFIX) - 1); - int ret = os_snprintf(outp, fmtspace, fmt, len, p); - if (ret >= fmtspace) { -toobig: errmsg_warnx("skipping an overly long search path"); - return; - } - outp += ret; - if (!*gameinfo_clientlib) { - os_strcpy(outp, OS_LIT("client" OS_DLSUFFIX)); - trygamelib(bindir, clientlib); - } - if (!*gameinfo_serverlib) { - os_strcpy(outp, OS_LIT("server" OS_DLSUFFIX)); - trygamelib(bindir, serverlib); - } -} - -// state for the callback below to keep it somewhat reentrant (except where -// it's not because I got lazy and wrote some spaghetti) -struct kv_parsestate { - const os_char *cwd; - // after parsing a key we *don't* care about, how many nested subkeys have - // we come across? - short dontcarelvl; - // after parsing a key we *do* care about, which key in the matchkeys[] - // array below are we looking for next? - schar nestlvl; - // what kind of key did we just match? - schar matchtype; -}; - -// this is a sprawling mess. Too Bad! -static void kv_cb(enum kv_token type, const char *p, uint len, void *_ctxt) { - struct kv_parsestate *ctxt = _ctxt; - - static const struct { - const char *s; - uint len; - } matchkeys[] = { - {"gameinfo", 8}, - {"filesystem", 10}, - {"searchpaths", 11} - }; - - // values for ctxt->matchtype - enum { mt_none, mt_title, mt_nest, mt_game, mt_gamebin }; - - #define MATCH(s) (len == sizeof(s) - 1 && matchtok(p, s, sizeof(s) - 1)) - switch (type) { - case KV_IDENT: case KV_IDENT_QUOTED: - if (ctxt->nestlvl == 1 && MATCH("game")) { - ctxt->matchtype = mt_title; - } - else if (ctxt->nestlvl == 3) { - // for some reason there's a million different ways of - // specifying the same type of path - if (MATCH("mod+game") || MATCH("game+mod") || MATCH("game") || - MATCH("mod")) { - ctxt->matchtype = mt_game; - } - else if (MATCH("gamebin")) { - ctxt->matchtype = mt_gamebin; - } - } - else if (len == matchkeys[ctxt->nestlvl].len && - matchtok(p, matchkeys[ctxt->nestlvl].s, len)) { - ctxt->matchtype = mt_nest; - } - break; - case KV_NEST_START: - if (ctxt->matchtype == mt_nest) ++ctxt->nestlvl; - else ++ctxt->dontcarelvl; - ctxt->matchtype = mt_none; - break; - case KV_VAL: case KV_VAL_QUOTED: - if (ctxt->dontcarelvl) break; - // dumb hack: ignore Survivors title (they left it set to "Left 4 - // Dead 2" but it clearly isn't Left 4 Dead 2) - if (ctxt->matchtype == mt_title && !GAMETYPE_MATCHES(L4DS)) { - // title really shouldn't get this long, but truncate just to - // avoid any trouble... - // also note: leaving 1 byte of space for null termination (the - // buffer is already zeroed initially) - if (len > sizeof(title) - 1) len = sizeof(title) - 1; - memcpy(title, p, len); - } - else if (ctxt->matchtype == mt_game || - ctxt->matchtype == mt_gamebin) { - // if we already have everything, we can just stop! - if (*gameinfo_clientlib && *gameinfo_serverlib) break; - dolibsearch(p, len, ctxt->matchtype == mt_gamebin, ctxt->cwd); - } - ctxt->matchtype = mt_none; - break; - case KV_NEST_END: - if (ctxt->dontcarelvl) --ctxt->dontcarelvl; else --ctxt->nestlvl; - break; - case KV_COND_PREFIX: case KV_COND_SUFFIX: - errmsg_warnx("just ignoring conditional \"%.*s\"", len, p); - } - #undef MATCH -} - DECL_VFUNC_DYN(const char *, GetGameDirectory) bool gameinfo_init(void) { @@ -242,76 +41,39 @@ bool gameinfo_init(void) { return false; } - // engine always calls chdir() with its own base path on startup, so engine - // base dir is just cwd - os_char cwd[PATH_MAX]; - if (!os_getcwd(cwd, sizeof(cwd) / sizeof(*cwd))) { - errmsg_errorstd("couldn't get working directory"); - return false; - } - int len = os_strlen(cwd); - if (len + sizeof("/bin") > sizeof(bindir) / sizeof(*bindir)) { - errmsg_errorx("working directory path is too long!"); - return false; - } - memcpy(bindir, cwd, len * sizeof(*cwd)); - memcpy(bindir + len, PATHSEP OS_LIT("bin"), 5 * sizeof(os_char)); - #ifdef _WIN32 // Although the engine itself uses Unicode-incompatible stuff everywhere so // supporting arbitrary paths is basically a no-go, turns out we still have - // to respect the system code page setting, otherwise some users using e.g. - // Cyrillic folder names and successfully loading their speedgames won't be - // able to load SST. Thanks Windows! + // to respect the system legacy code page setting, otherwise some users + // using e.g. Cyrillic folder names and successfully loading their + // speedgames won't be able to load SST. Thanks Windows! const char *lcpgamedir = GetGameDirectory(engclient); - int gamedirlen = MultiByteToWideChar(CP_ACP, 0, lcpgamedir, - strlen(lcpgamedir), gamedir, sizeof(gamedir) / sizeof(*gamedir)); - if (!gamedirlen) { + if (!MultiByteToWideChar(CP_ACP, 0, lcpgamedir, strlen(lcpgamedir), gamedir, + sizeof(gamedir) / sizeof(*gamedir))) { errmsg_errorsys("couldn't convert game directory path character set"); return false; } #else // no need to munge charset, use the string pointer directly gameinfo_gamedir = GetGameDirectory(engclient); - int gamedirlen = strlen(gameinfo_gamedir); #endif - os_char gameinfopath[PATH_MAX]; - if (gamedirlen + sizeof("/gameinfo.txt") > sizeof(gameinfopath) / - sizeof(*gameinfopath)) { - errmsg_errorx("game directory path is too long!"); - return false; - } - memcpy(gameinfopath, gameinfo_gamedir, gamedirlen * - sizeof(*gameinfo_gamedir)); - memcpy(gameinfopath + gamedirlen, PATHSEP OS_LIT("gameinfo.txt"), - 14 * sizeof(os_char)); - int fd = os_open(gameinfopath, O_RDONLY); - if (fd == -1) { - errmsg_errorstd("couldn't open gameinfo.txt"); - return false; + + // dumb hack: ignore Survivors title (they left it set to "Left 4 Dead 2" + // but that game clearly isn't Left 4 Dead 2) + if (GAMETYPE_MATCHES(L4DS)) { + gameinfo_title = "Left 4 Dead: Survivors"; } - char buf[1024]; - struct kv_parser kvp = {0}; - struct kv_parsestate ctxt = {.cwd = cwd}; - int nread; - while (nread = read(fd, buf, sizeof(buf))) { - if (nread == -1) { - errmsg_errorstd("couldn't read gameinfo.txt"); - goto e; - } - if (!kv_parser_feed(&kvp, buf, nread, &kv_cb, &ctxt)) goto ep; + else { +#ifdef _WIN32 + // XXX: this same FindWindow call happens in ac.c - maybe factor out? + void *gamewin = FindWindowW(L"Valve001", 0); + // assuming: all games/mods use narrow chars only; this won't fail. + GetWindowTextA(gamewin, title, sizeof(title)); +#else +#erorr TODO(linux): grab window handle and title from SDL (a bit involved...) +#endif } - if (!kv_parser_done(&kvp)) goto ep; - close(fd); - - // dumb hack pt2, see also kv callback above - if (GAMETYPE_MATCHES(L4DS)) gameinfo_title = "Left 4 Dead: Survivors"; return true; - -ep: errmsg_errorx("couldn't parse gameinfo.txt (%d:%d): %s", kvp.line, kvp.col, - kvp.errmsg); -e: close(fd); - return false; } // vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/gameinfo.h b/src/gameinfo.h index 4948268..76c3c57 100644 --- a/src/gameinfo.h +++ b/src/gameinfo.h @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith + * Copyright © 2023 Michael Smith * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -22,18 +22,11 @@ #include "intdefs.h" #include "os.h" -/* These variables are only set after calling gameinfo_init(). */ -extern const os_char *gameinfo_bindir; /* Absolute path to top-level bin/ */ +/* Miscellaneous metadata variables about the currently running game */ extern const os_char *gameinfo_gamedir; /* Absolute path to game directory */ extern const char *gameinfo_title; /* Name of the game (window title) */ -extern const os_char *gameinfo_clientlib; /* Absolute path to the client lib */ -extern const os_char *gameinfo_serverlib; /* Absolute path to the server lib */ -/* - * This function is called early in the plugin load and does a whole bunch of - * spaghetti magic to figure out which game/engine we're in and where its - * libraries (which we want to hook) are located. - */ +/* Called early in plugin initialisation to set up the variables above. */ bool gameinfo_init(void); #endif diff --git a/src/l4dmm.c b/src/l4dmm.c index 3c7a07c..d391584 100644 --- a/src/l4dmm.c +++ b/src/l4dmm.c @@ -95,13 +95,7 @@ const char *l4dmm_curcampaign(void) { } INIT { - // XXX: ugh, we NEED to centralise library stuff at some point, this sucks -#ifdef _WIN32 - void *mmlib = GetModuleHandleW(L"matchmaking.dll"); -#else - void *mmlib = dlopen("matchmaking.so", RTLD_NOW | RTLD_NOLOAD); - if (mmlib) dlclose(mmlib); -#endif + void *mmlib = os_dlhandle(OS_LIT("matchmaking") OS_LIT(OS_DLSUFFIX)); if (mmlib) { ifacefactory factory = (ifacefactory)os_dlsym(mmlib, "CreateInterface"); if (!factory) { diff --git a/src/os-unix.h b/src/os-unix.h index ec9a940..a25d8ed 100644 --- a/src/os-unix.h +++ b/src/os-unix.h @@ -39,10 +39,19 @@ typedef char os_char; #define os_getenv getenv #define os_getcwd getcwd +#define OS_DLPREFIX "lib" #define OS_DLSUFFIX ".so" #define OS_MAIN main +static inline void *os_dlopen(const char *name) { + return dlopen(name, RTLD_NOW); +} +static inline void *os_dlhandle(const char *name) { + void *ret = dlopen(name, RTLD_NOW | RTLD_NOLOAD); + if (ret) dlclose(ret); + return ret; +} #define os_dlsym dlsym #ifdef __linux__ diff --git a/src/os-win32.h b/src/os-win32.h index fe61f84..a006083 100644 --- a/src/os-win32.h +++ b/src/os-win32.h @@ -45,10 +45,17 @@ typedef unsigned short os_char; #define os_getenv _wgetenv #define os_getcwd _wgetcwd +#define OS_DLPREFIX "" #define OS_DLSUFFIX ".dll" #define OS_MAIN wmain +static inline void *os_dlopen(const ushort *name) { + return LoadLibraryW(name); +} +static inline void *os_dlhandle(const ushort *name) { + return GetModuleHandleW(name); +} static inline void *os_dlsym(void *m, const char *s) { return (void *)GetProcAddress(m, s); } diff --git a/src/sst.c b/src/sst.c index 9dc8829..e00451a 100644 --- a/src/sst.c +++ b/src/sst.c @@ -20,7 +20,6 @@ #include #endif -#include "ac.h" #include "con_.h" #include "engineapi.h" #include "errmsg.h" @@ -70,22 +69,63 @@ static inline void *ownhandle(void) { #define VDFBASENAME "SourceSpeedrunTools" +#ifdef _WIN32 +// not a proper check, just a short-circuit check to avoid doing more work. +static inline bool checksamedrive(const ushort *restrict path1, + const ushort *restrict path2) { + bool ret = (path1[0] | 32) == (path2[0] | 32); + if (!ret) errmsg_errorx("game and plugin must be on the same drive\n"); + return ret; +} +#endif + DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { - // note: gamedir doesn't account for if the dll is in a base mod's - // directory, although it will yield a valid/working relative path anyway. - const os_char *searchdir = ifacever == 3 ? - gameinfo_gamedir : gameinfo_bindir; os_char path[PATH_MAX]; if (os_dlfile(ownhandle(), path, sizeof(path) / sizeof(*path)) == -1) { // hopefully by this point this won't happen, but, like, never know errmsg_errordl("failed to get path to plugin"); return; } + os_char _startdir[PATH_MAX]; + const os_char *startdir; + if (ifacever == 2) { + startdir = _startdir; + os_getcwd(_startdir, PATH_MAX); // if this fails, OS devs are all fired. +#ifdef _WIN32 + // note: strictly speaking we *could* allow this with an absolute path + // since old builds allow absolute plugin_load paths but since it's less + // reliable if e.g. a disk is removed, and also doesn't work for all + // games, just rule it out entirely to keep things simple. + if (!checksamedrive(path, startdir)) return; +#endif + int len = os_strlen(startdir); + if (len + sizeof("/bin") >= PATH_MAX) { + errmsg_errorx("path to game is too long"); + return; + } + memcpy(_startdir + len, OS_LIT("/bin"), 5 * sizeof(os_char)); + } + else /* ifacever == 3 */ { + // newer games load from the mod dir instead of engine bin, and search + // in inherited search paths too, although we don't bother with those as + // the actual VDF is only read from the mod itself so it's always enough + // to make the path relative to that (and that makes the actual plugin + // search fast too as it should find it in the first place it looks). + // we *still* refuse to autoload across different drives even if some + // obscure gameinfo.txt arrangement could technically allow that to work + startdir = gameinfo_gamedir; +#ifdef _WIN32 + if (!checksamedrive(path, startdir)) return; +#endif + } os_char relpath[PATH_MAX]; #ifdef _WIN32 - if (!PathRelativePathToW(relpath, searchdir, FILE_ATTRIBUTE_DIRECTORY, + // note: dll isn't actually in gamedir if it's in a base mod directory + // note: gamedir doesn't account for if the dll is in a base mod's + // directory, although it will yield a valid/working relative path anyway. + if (!PathRelativePathToW(relpath, startdir, FILE_ATTRIBUTE_DIRECTORY, path, 0)) { - errmsg_errorsys("couldn't compute a relative path for some reason"); + errmsg_errorsys("couldn't compute a relative path"); return; } // arbitrary aesthetic judgement @@ -113,9 +153,8 @@ DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { errmsg_errorstd("couldn't open %" fS, path); return; } - // XXX: oh, crap, we're clobbering unicode again. welp, let's hope the - // theory that the engine is just as bad if not worse is true so that it - // doesn't matter. + // XXX: oh crap, we're clobbering unicode again. welp, let's continue + // relying on the theory that the engine would fail to deal with it anyway. if (fprintf(f, "Plugin { file \"%" fS "\" }\n", relpath) < 0 || fflush(f) == -1) { errmsg_errorstd("couldn't write to %" fS, path); @@ -183,6 +222,27 @@ static const char *updatenotes = "\ #include // generated by build/codegen.c static void do_featureinit(void) { + // load libs that might not be there early (...at least on Linux???) + clientlib = os_dlhandle(OS_LIT("client") OS_LIT(OS_DLSUFFIX)); + if (!clientlib) { + errmsg_warndl("couldn't get the game's client library"); + } + else if (!(factory_client = (ifacefactory)os_dlsym(clientlib, + "CreateInterface"))) { + errmsg_warndl("couldn't get client's CreateInterface"); + } + void *inputsystemlib = os_dlhandle(OS_LIT("bin/") OS_LIT(OS_DLPREFIX) + OS_LIT("inputsystem") OS_LIT(OS_DLSUFFIX)); + if (!inputsystemlib) { + errmsg_warndl("couldn't get the input system library"); + } + else if (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib, + "CreateInterface"))) { + errmsg_warndl("couldn't get input system's CreateInterface"); + } + inputsystem = factory_inputsystem("InputSystemVersion001", 0); + if (!inputsystem) errmsg_warnx("missing input system interface"); + // ... and now for the real magic! initfeatures(); // if we're autoloaded and the external autoupdate script downloaded a new @@ -254,42 +314,8 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { errmsg_warnsys("couldn't set up memory for function hooking"); return false; } - factory_engine = enginef; factory_server = serverf; -#ifdef _WIN32 - void *inputsystemlib = GetModuleHandleW(L"inputsystem.dll"); -#else - // TODO(linux): assuming the above doesn't apply to this; check if it does! - // ... actually, there's a good chance this assumption is now wrong! - void *inputsystemlib = dlopen("bin/libinputsystem.so", - RTLD_NOW | RLTD_NOLOAD); - if (inputsystemlib) dlclose(inputsystemlib); // blegh -#endif - if (!inputsystemlib) { - errmsg_warndl("couldn't get the input system library"); - } - else if (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib, - "CreateInterface"))) { - errmsg_warndl("couldn't get input system's CreateInterface"); - } if (!engineapi_init(ifacever)) return false; -#ifdef _WIN32 - clientlib = GetModuleHandleW(gameinfo_clientlib); -#else - // Apparently on Linux, the client library isn't actually loaded yet here, - // so RTLD_NOLOAD won't actually find it. We have to just dlopen it - // normally - and then remember to decrement the refcount again later in - // do_unload() so nothing gets leaked! - clientlib = dlopen(gameinfo_clientlib, RTLD_NOW); -#endif - if (!clientlib) { - errmsg_warndl("couldn't get the game's client library"); - } - else if (!(factory_client = (ifacefactory)os_dlsym(clientlib, - "CreateInterface"))) { - errmsg_warndl("couldn't get client's CreateInterface"); - } - const void **p = vtable_firstdiff; if (GAMETYPE_MATCHES(Portal2)) *p++ = (void *)&nop_p_v; // ClientFullyConnect *p++ = (void *)&nop_p_v; // ClientDisconnect @@ -303,7 +329,6 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { *p++ = (void *)&nop_ipipp_v; // OnQueryCvarValueFinished (002+) *p++ = (void *)&nop_p_v; // OnEdictAllocated *p = (void *)&nop_p_v; // OnEdictFreed - if (!deferinit()) { do_featureinit(); fixes_apply(); } return true; } -- cgit v1.2.3