/* * 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 * 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 #ifdef _WIN32 #include #endif #include "ac.h" #include "con_.h" #include "engineapi.h" #include "errmsg.h" #include "event.h" #include "fixes.h" #include "gamedata.h" #include "gameinfo.h" #include "gametype.h" #include "hook.h" #include "os.h" #include "vcall.h" #include "version.h" #ifdef _WIN32 #define fS "S" #else #define fS "s" #endif static int ifacever; // XXX: exposing this clumsily to portalcolours. we should have a better way of // exposing lib handles in general, probably. void *clientlib = 0; bool sst_earlyloaded = false; // see deferinit() below #ifdef _WIN32 extern long __ImageBase; // this is actually the PE header struct but don't care #define ownhandle() ((void *)&__ImageBase) #else // sigh, _GNU_SOURCE crap. define here instead >:( typedef struct { const char *dli_fname; void *dli_fbase; const char *dli_sname; void *dli_saddr; } Dl_info; int dladdr1(const void *addr, Dl_info *info, void **extra_info, int flags); static inline void *ownhandle(void) { Dl_info dontcare; void *dl; dladdr1((void *)&ownhandle, &dontcare, &dl, /*RTLD_DL_LINKMAP*/ 2); return dl; } #endif #define VDFBASENAME "SourceSpeedrunTools" 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 relpath[PATH_MAX]; #ifdef _WIN32 if (!PathRelativePathToW(relpath, searchdir, FILE_ATTRIBUTE_DIRECTORY, path, 0)) { errmsg_errorsys("couldn't compute a relative path for some reason"); return; } // arbitrary aesthetic judgement for (os_char *p = relpath; *p; ++p) if (*p == L'\\') *p = L'/'; #else #error TODO(linux): implement this, it's late right now and I can't be bothered #endif int len = os_strlen(gameinfo_gamedir); if (len + sizeof("/addons/" VDFBASENAME ".vdf") > sizeof(path) / sizeof(*path)) { errmsg_errorx("path to VDF is too long"); return; } memcpy(path, gameinfo_gamedir, len * sizeof(*gameinfo_gamedir)); memcpy(path + len, OS_LIT("/addons"), 8 * sizeof(os_char)); if (os_mkdir(path) == -1 && errno != EEXIST) { errmsg_errorstd("couldn't create %" fS, path); return; } memcpy(path + len + sizeof("/addons") - 1, OS_LIT("/") OS_LIT(VDFBASENAME) OS_LIT(".vdf"), sizeof("/" VDFBASENAME ".vdf") * sizeof(os_char)); FILE *f = os_fopen(path, OS_LIT("wb")); if (!f) { 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. if (fprintf(f, "Plugin { file \"%" fS "\" }\n", relpath) < 0 || fflush(f) == -1) { errmsg_errorstd("couldn't write to %" fS, path); } fclose(f); } DEF_CCMD_HERE(sst_autoload_disable, "Stop loading SST on game startup", 0) { os_char path[PATH_MAX]; int len = os_strlen(gameinfo_gamedir); if (len + sizeof("/addons/" VDFBASENAME ".vdf") > sizeof(path) / sizeof(*path)) { errmsg_errorx("path to VDF is too long"); return; } memcpy(path, gameinfo_gamedir, len * sizeof(*gameinfo_gamedir)); memcpy(path + len, OS_LIT("/addons/") OS_LIT(VDFBASENAME) OS_LIT(".vdf"), sizeof("/addons/" VDFBASENAME ".vdf") * sizeof(os_char)); if (os_unlink(path) == -1 && errno != ENOENT) { errmsg_warnstd("couldn't delete %" fS, path); } } DEF_CCMD_HERE(sst_printversion, "Display plugin version information", 0) { con_msg("v" VERSION "\n"); } // most plugin callbacks are unused - define dummy functions for each signature static void VCALLCONV nop_v_v(void *this) {} static void VCALLCONV nop_p_v(void *this, void *p) {} static void VCALLCONV nop_pp_v(void *this, void *p1, void *p2) {} static void VCALLCONV nop_pii_v(void *this, void *p, int i1, int i2) {} static int VCALLCONV nop_p_i(void *this, void *p) { return 0; } static int VCALLCONV nop_pp_i(void *this, void *p1, void *p2) { return 0; } static int VCALLCONV nop_5pi_i(void *this, void *p1, void *p2, void *p3, void *p4, void *p5, int i) { return 0; } static void VCALLCONV nop_ipipp_v(void *this, int i1, void *p1, int i2, void *p2, void *p3) {} // more source spaghetti wow! static void VCALLCONV SetCommandClient(void *this, int i) { con_cmdclient = i; } // this is where we start dynamically adding virtual functions, see vtable[] // array below static const void **vtable_firstdiff; static const void *const *const plugin_obj; static bool already_loaded = false, skip_unload = false; // auto-update message. see below in do_featureinit() static const char *updatenotes = "\ * various internal cleanup\n\ "; #include // generated by build/codegen.c static void do_featureinit(void) { initfeatures(); // if we're autoloaded and the external autoupdate script downloaded a new // version, let the user know about the cool new stuff! if (getenv("SST_UPDATED")) { // avoid displaying again if we're unloaded and reloaded in one session #ifdef _WIN32 SetEnvironmentVariableA("SST_UPDATED", 0); #else unsetenv("SST_UPDATED"); #endif struct rgba gold = {255, 210, 0, 255}; struct rgba white = {255, 255, 255, 255}; con_colourmsg(&white, "\n" NAME " was just "); con_colourmsg(&gold, "UPDATED"); con_colourmsg(&white, " to version "); con_colourmsg(&gold, "%s", VERSION); con_colourmsg(&white, "!\n\nNew in this version:\n%s\n", updatenotes); } } typedef void (*VCALLCONV VGuiConnect_func)(void *this); static VGuiConnect_func orig_VGuiConnect; static void VCALLCONV hook_VGuiConnect(void *this) { orig_VGuiConnect(this); do_featureinit(); fixes_apply(); unhook_vtable(*(void ***)vgui, vtidx_VGuiConnect, (void *)orig_VGuiConnect); } DECL_VFUNC_DYN(bool, VGuiIsInitialized) // --- Magical deferred load order hack nonsense! --- // VDF plugins load right after server.dll, but long before most other stuff. We // want to be able to load via VDF so archived cvars in config.cfg can get set, // but don't want to be so early that most of the game's interfaces haven't been // brought up yet. Hook CEngineVGui::Connect(), which is called very late in // startup, in order to init the features properly. // // Route credit to bill for helping figure a lot of this out - mike static bool deferinit(void) { if (!vgui) { errmsg_warnx("can't use VEngineVGui for deferred feature setup"); goto e; } // Arbitrary check to infer whether we've been early- or late-loaded. // We used to just see whether gameui.dll/libgameui.so was loaded, but // Portal 2 does away with the separate gameui library, so now we just call // CEngineVGui::IsInitialized() which works everywhere. if (VGuiIsInitialized(vgui)) return false; sst_earlyloaded = true; // let other code know if (!os_mprot(*(void ***)vgui + vtidx_VGuiConnect, sizeof(void *), PAGE_READWRITE)) { errmsg_warnsys("couldn't make CEngineVGui vtable writable for deferred " "feature setup"); goto e; } orig_VGuiConnect = (VGuiConnect_func)hook_vtable(*(void ***)vgui, vtidx_VGuiConnect, (void *)&hook_VGuiConnect); return true; e: con_warn("!!! SOME FEATURES MAY BE BROKEN !!!\n"); // Lesser of two evils: just init features now. Unlikely to happen anyway. return false; } static bool do_load(ifacefactory enginef, ifacefactory serverf) { if (!hook_init()) { 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 *p++ = (void *)&nop_pp_v; // ClientPutInServer *p++ = (void *)&SetCommandClient; // SetCommandClient *p++ = (void *)&nop_p_v; // ClientSettingsChanged *p++ = (void *)&nop_5pi_i; // ClientConnect *p++ = ifacever > 1 ? (void *)&nop_pp_i : (void *)&nop_p_i; // ClientCommand // remaining stuff here is backwards compatible, so added unconditionally *p++ = (void *)&nop_pp_i; // NetworkIDValidated *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; } struct CServerPlugin /* : IServerPluginHelpers */ { void **vtable; struct CUtlVector plugins; /*IPluginHelpersCheck*/ void *pluginhlpchk; }; struct CPlugin { char description[128]; bool paused; void *theplugin; // our own "this" pointer (or whichever other plugin it is) int ifacever; // should be the plugin library, but in old Source branches it's just null, // because CServerPlugin::Load() erroneously shadows this field with a local void *module; }; static void do_unload(void) { #ifdef _WIN32 // this is only relevant in builds that predate linux support struct CServerPlugin *pluginhandler = factory_engine("ISERVERPLUGINHELPERS001", 0); if (pluginhandler) { // if not, oh well too bad we tried :^) struct CPlugin **plugins = pluginhandler->plugins.m.mem; int n = pluginhandler->plugins.sz; for (struct CPlugin **pp = plugins; pp - plugins < n; ++pp) { if ((*pp)->theplugin == (void *)&plugin_obj) { // see comment in CPlugin above. setting this to the real handle // right before the engine tries to unload us allows it to // actually do so. in newer branches this is redundant but // doesn't do any harm so it's just unconditional. // NOTE: old engines ALSO just leak the handle and never call // Unload() if Load() fails; can't really do anything about that (*pp)->module = ownhandle(); break; } } } #endif endfeatures(); #ifdef __linux__ if (clientlib) dlclose(clientlib); #endif con_disconnect(); } static bool VCALLCONV Load(void *this, ifacefactory enginef, ifacefactory serverf) { if (already_loaded) { con_warn("Already loaded! Doing nothing!\n"); skip_unload = true; return false; } already_loaded = do_load(enginef, serverf); skip_unload = !already_loaded; return already_loaded; } static void VCALLCONV Unload(void *this) { // the game tries to unload on a failed load, for some reason if (skip_unload) { skip_unload = false; return; } do_unload(); } static void VCALLCONV Pause(void *this) { con_warn(NAME " doesn't support plugin_pause - ignoring\n"); } static void VCALLCONV UnPause(void *this) { con_warn(NAME " doesn't support plugin_unpause - ignoring\n"); } static const char *VCALLCONV GetPluginDescription(void *this) { return LONGNAME " v" VERSION; } DECL_VFUNC_DYN(void, ServerCommand, const char *) DEF_EVENT(ClientActive, struct edict */*player*/) DEF_EVENT(Tick, bool /*simulating*/) // Quick and easy server tick event. Eventually, we might want a deeper hook // for anything timing-sensitive, but this will do for our current needs. static void VCALLCONV GameFrame(void *this, bool simulating) { EMIT_Tick(simulating); } static void VCALLCONV ClientActive(void *this, struct edict *player) { EMIT_ClientActive(player); } #define MAX_VTABLE_FUNCS 21 static const void *vtable[MAX_VTABLE_FUNCS] = { // start off with the members which (thankfully...) are totally stable // between interface versions - the *remaining* members get filled in just // in time by do_load() once we've figured out what engine branch we're on (void *)&Load, (void *)&Unload, (void *)&Pause, (void *)&UnPause, (void *)&GetPluginDescription, (void *)&nop_p_v, // LevelInit (void *)&nop_pii_v, // ServerActivate (void *)&GameFrame, (void *)&nop_v_v, // LevelShutdown (void *)&ClientActive // At this point, Alien Swarm and Portal 2 add ClientFullyConnect, so we // can't hardcode any more of the layout! }; // end MUST point AFTER the last of the above entries static const void **vtable_firstdiff = vtable + 10; // this is equivalent to a class with no members! static const void *const *const plugin_obj = vtable; EXPORT const void *CreateInterface(const char *name, int *ret) { if (!strncmp(name, "ISERVERPLUGINCALLBACKS00", 24)) { if (name[24] >= '1' && name[24] <= '3' && name[25] == '\0') { if (ret) *ret = 0; ifacever = name[24] - '0'; return &plugin_obj; } } if (ret) *ret = 1; return 0; } // no better place to put this lol #include // generated by src/build/codegen.c // vi: sw=4 ts=4 noet tw=80 cc=80