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/gameinfo.c | 276 ++++----------------------------------------------------- 1 file changed, 19 insertions(+), 257 deletions(-) (limited to 'src/gameinfo.c') 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 -- cgit v1.2.3