diff options
Diffstat (limited to 'src/gameinfo.c')
-rw-r--r-- | src/gameinfo.c | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/src/gameinfo.c b/src/gameinfo.c new file mode 100644 index 0000000..32f5051 --- /dev/null +++ b/src/gameinfo.c @@ -0,0 +1,372 @@ +/* + * Copyright © 2021 Michael Smith <mikesmiffy128@gmail.com> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdbool.h> +#ifdef _WIN32 +#include <shlwapi.h> +#endif + +#include "con_.h" +#include "intdefs.h" +#include "kv.h" +#include "os.h" + +// Formatting for os_char * -> char * (or vice versa) - needed for con_warn()s +// with file paths, etc +#ifdef _WIN32 +#define fS "S" // os string (wide string) to regular string +#define Fs L"S" // regular string to os string (wide string) +#else +// everything is just a regular string already +#define fS "s" +#define Fs "s" +#endif + +static os_char exedir[PATH_MAX]; +static os_char gamedir[PATH_MAX]; +static char _gameinfo_title[64] = {0}; +const char *gameinfo_title = _gameinfo_title; +static os_char _gameinfo_clientlib[PATH_MAX] = {0}; +const os_char *gameinfo_clientlib = _gameinfo_clientlib; +static os_char _gameinfo_serverlib[PATH_MAX] = {0}; +const os_char *gameinfo_serverlib = _gameinfo_serverlib; + +// magical argc/argv grabber so we don't have to go through procfs +#ifdef __linux__ +static const char *prog_argv; +static int storeargs(int argc, char *argv[]) { + prog_argv = argv; + return 0; +} +__attribute__((used, section(".init_array"))) +static void *pstoreargs = &storeargs; +#endif + +// 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 try_gamelib(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) { + con_warn("gameinfo: failed to access %" fS ": %s\n", path, + strerror(errno)); + } +} + +// note: p and len are a non-null-terminated string +static inline void do_gamelib_search(const char *p, uint len, bool isgamebin) { + // 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, exedir); + 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. if I'm wrong this can + // just change later to an explicit charset conversion, so... it's kinda + // whatever, I guess + 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: con_warn("gameinfo: skipping an overly long search path\n"); + return; + } + outp += ret; + if (!*gameinfo_clientlib) { + os_strcpy(outp, OS_LIT("client" OS_DLSUFFIX)); + try_gamelib(bindir, _gameinfo_clientlib); + } + if (!*gameinfo_serverlib) { + os_strcpy(outp, OS_LIT("server" OS_DLSUFFIX)); + try_gamelib(bindir, _gameinfo_serverlib); + } +} + +// state for the callback below to keep it somewhat reentrant-ish (except where +// it isn't because I got lazy and wrote some spaghetti) +struct kv_parsestate { + // 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; + if (ctxt->matchtype == mt_title) { + // 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(_gameinfo_title) - 1) { + len = sizeof(_gameinfo_title) - 1; + } + memcpy(_gameinfo_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; + do_gamelib_search(p, len, ctxt->matchtype == mt_gamebin); + } + ctxt->matchtype = mt_none; + break; + case KV_NEST_END: + if (ctxt->dontcarelvl) --ctxt->dontcarelvl; else --ctxt->nestlvl; + } + #undef MATCH +} + +bool gameinfo_init(void) { + const os_char *modname = OS_LIT("hl2"); +#ifdef _WIN32 + int len = GetModuleFileNameW(0, exedir, PATH_MAX); + if (!len) { + char err[128]; + OS_WINDOWS_ERROR(err); + con_warn("gameinfo: couldn't get EXE path: %s\n", err); + return false; + } + // if the buffer is full and has no null, it's truncated + if (len == PATH_MAX && exedir[len - 1] != L'\0') { + con_warn("gameinfo: EXE path is too long!\n"); + return false; + } +#else + int len = readlink("/proc/self/exe", exedir, PATH_MAX); + if (len == -1) { + con_warn("gameinfo: couldn't get program path: %s\n", strerror(errno)); + return false; + } + // if the buffer is full at all, it's truncated (readlink never writes \0) + if (len == PATH_MAX) { + con_warn("gameinfo: program path is too long!\n"); + return false; + } + else { + exedir[len] = '\0'; + } +#endif + // find the last slash + os_char *p; + for (p = exedir + len - 1; *p != OS_LIT('/') +#ifdef _WIN32 + && *p != L'\\' +#endif + ; --p); + // ... and split on it + *p = 0; + const os_char *exename = p + 1; +#ifdef _WIN32 + // try and infer the default mod name (when -game isn't given) from the exe + // name for a few known games + if (!_wcsicmp(exename, L"left4dead2.exe")) modname = L"left4dead2"; + else if (!_wcsicmp(exename, L"left4dead.exe")) modname = L"left4dead"; + else if (!_wcsicmp(exename, L"portal2.exe")) modname = L"portal2"; + + const ushort *args = GetCommandLineW(); + const ushort *argp = args; + ushort modbuf[PATH_MAX]; + // have to take the _last_ occurence of -game because sourcemods get the + // flag twice, for some reason + while (argp = wcsstr(argp, L" -game ")) { + argp += 7; + while (*argp == L' ') ++argp; + ushort sep = L' '; + // WARNING: not handling escaped quotes and such nonsense, since you + // can't have quotes in filepaths anyway outside of UNC and I'm just + // assuming there's no way Source could even be started with such an + // insanely named mod. We'll see how this assumption holds up! + if (*argp == L'"') { + ++argp; + sep = L'"'; + } + ushort *bufp = modbuf; + for (; *argp != L'\0' && *argp != sep; ++argp, ++bufp) { + if (bufp - modbuf == PATH_MAX - 1) { + con_warn("gameinfo: mod name parameter is too long\n"); + return false; + } + *bufp = *argp; + } + *bufp = L'\0'; + modname = modbuf; + } + bool isrelative = PathIsRelativeW(modname); +#else + // also do the executable name check just for portal2_linux + if (!strcmp(exename, "portal2_linux")) modname = "portal2"; + // ah, the sane, straightforward world of unix command line arguments :) + for (char **pp = prog_argv + 1; *pp; ++pp) { + if (!strcmp(*pp, "-game")) { + if (!*++pp) break; + modname = *pp; + } + } + // ah, the sane, straightforward world of unix paths :) + bool isrelative = modname[0] != '/'; +#endif + + int ret = isrelative ? + os_snprintf(gamedir, PATH_MAX, OS_LIT("%s/%s"), exedir, modname) : + // mod name might actually be an absolute (if installed in steam + // sourcemods for example) + os_snprintf(gamedir, PATH_MAX, OS_LIT("%s"), modname); + if (ret >= PATH_MAX) { + con_warn("gameinfo: game directory path is too long!\n"); + return false; + } + os_char gameinfopath[PATH_MAX]; + if (os_snprintf(gameinfopath, PATH_MAX, OS_LIT("%s/gameinfo.txt"), + gamedir, modname) >= PATH_MAX) { + con_warn("gameinfo: gameinfo.text path is too long!\n"); + return false; + } + + int fd = os_open(gameinfopath, O_RDONLY); + if (fd == -1) { + con_warn("gameinfo: couldn't open gameinfo.txt: %s\n", strerror(errno)); + return false; + } + char buf[1024]; + struct kv_parser kvp = {0}; + struct kv_parsestate ctxt = {0}; + int nread; + while (nread = read(fd, buf, sizeof(buf))) { + if (nread == -1) { + con_warn("gameinfo: couldn't read gameinfo.txt: %s\n", + strerror(errno)); + goto e; + } + kv_parser_feed(&kvp, buf, nread, &kv_cb, &ctxt); + if (kvp.state == KV_PARSER_ERROR) goto ep; + } + kv_parser_done(&kvp); + if (kvp.state == KV_PARSER_ERROR) goto ep; + + close(fd); + return true; + +ep: con_warn("gameinfo: couldn't parse gameinfo.txt (%d:%d): %s\n", + kvp.line, kvp.col, kvp.errmsg); +e: close(fd); + return false; +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 |