diff options
-rwxr-xr-x | compile | 7 | ||||
-rw-r--r-- | compile.bat | 6 | ||||
-rw-r--r-- | gamedata/matchmaking.kv | 11 | ||||
-rw-r--r-- | src/kvsys.c | 98 | ||||
-rw-r--r-- | src/kvsys.h | 59 | ||||
-rw-r--r-- | src/l4d2vote.c | 134 | ||||
-rw-r--r-- | src/l4dmm.c | 134 | ||||
-rw-r--r-- | src/l4dmm.h | 32 | ||||
-rw-r--r-- | src/l4dreset.c | 243 | ||||
-rw-r--r-- | src/l4dwarp.c | 1 | ||||
-rw-r--r-- | src/rinput.c | 8 | ||||
-rw-r--r-- | src/sst.c | 36 |
12 files changed, 591 insertions, 178 deletions
@@ -67,7 +67,9 @@ src="\ gameinfo.c hook.c kv.c - l4d2vote.c + kvsys.c + l4dmm.c + l4dreset.c l4dwarp.c nosleep.c portalcolours.c @@ -85,7 +87,8 @@ $HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \ $HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \ -o .build/mkentprops src/build/mkentprops.c src/kv.c .build/codegen `for s in $src; do echo "src/$s"; done` -.build/mkgamedata gamedata/engine.kv gamedata/gamelib.kv gamedata/inputsystem.kv +.build/mkgamedata gamedata/engine.kv gamedata/gamelib.kv gamedata/inputsystem.kv \ + gamedata/matchmaking.kv .build/mkentprops gamedata/entprops.kv for s in $src; do cc "$s"; done $CC -shared -fpic -fuse-ld=lld -O0 -w -o .build/libtier0.so src/stubs/tier0.c diff --git a/compile.bat b/compile.bat index 60dba8f..660ebe7 100644 --- a/compile.bat +++ b/compile.bat @@ -72,7 +72,9 @@ setlocal DisableDelayedExpansion :+ gameinfo.c
:+ hook.c
:+ kv.c
-:+ l4d2vote.c
+:+ kvsys.c
+:+ l4dmm.c
+:+ l4dreset.c
:+ l4dwarp.c
:+ nomute.c
:+ nosleep.c
@@ -91,7 +93,7 @@ if "%dbg%"=="1" set src=%src% src/udis86.c %HOSTCC% -municode -O2 %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h -ladvapi32 ^
-o .build/mkentprops.exe src/build/mkentprops.c src/kv.c || exit /b
.build\codegen.exe%src% || exit /b
-.build\mkgamedata.exe gamedata/engine.kv gamedata/gamelib.kv gamedata/inputsystem.kv || exit /b
+.build\mkgamedata.exe gamedata/engine.kv gamedata/gamelib.kv gamedata/inputsystem.kv gamedata/matchmaking.kv || exit /b
.build\mkentprops.exe gamedata/entprops.kv || exit /b
llvm-rc /FO .build\dll.res src\dll.rc || exit /b
%CC% -shared -O0 -w -o .build/tier0.dll src/stubs/tier0.c
diff --git a/gamedata/matchmaking.kv b/gamedata/matchmaking.kv new file mode 100644 index 0000000..2cdf332 --- /dev/null +++ b/gamedata/matchmaking.kv @@ -0,0 +1,11 @@ +// = matchmaking library = + +// IMatchFramework +vtidx_GetMatchNetworkMsgController { + L4D 10 // NOTE: probably same for aswarm or p2 except with IAppSystem shift +} + +// IMatchNetworkMsgController +vtidx_GetActiveGameServerDetails { L4D 1 } + +// vi: sw=4 ts=4 noet tw=80 cc=80 ft=plain diff --git a/src/kvsys.c b/src/kvsys.c new file mode 100644 index 0000000..8bb140e --- /dev/null +++ b/src/kvsys.c @@ -0,0 +1,98 @@ +/* + * Copyright © 2023 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 "con_.h" +#include "engineapi.h" +#include "extmalloc.h" +#include "errmsg.h" +#include "feature.h" +#include "gametype.h" +#include "hook.h" +#include "kvsys.h" +#include "mem.h" +#include "os.h" +#include "vcall.h" + +FEATURE() + +IMPORT void *KeyValuesSystem(void); // vstlib symbol +static void *kvs; +DECL_VFUNC(int, GetSymbolForString, 3, const char *, bool) +DECL_VFUNC(const char *, GetStringForSymbol, 4, int) + +const char *kvsys_symtostr(int sym) { return GetStringForSymbol(kvs, sym); } +int kvsys_strtosym(const char *s) { return GetSymbolForString(kvs, s, true); } + +struct KeyValues *kvsys_getsubkey(struct KeyValues *kv, int sym) { + for (kv = kv->child; kv; kv = kv->next) if (kv->keyname == sym) return kv; + return 0; +} + +// this is trivial for now, but may need expansion later; see header comment +const char *kvsys_getstrval(struct KeyValues *kv) { return kv->strval; } + +void kvsys_free(struct KeyValues *kv) { + while (kv) { + kvsys_free(kv->child); + struct KeyValues *next = kv->next; + // NOTE! could (should?) call the free function in IKeyValuesSystem but + // we instead assume pooling is compiled out in favour of the IMemAlloc + // stuff, and thus call the latter directly for less overhead + extfree(kv->strval); extfree(kv->wstrval); + extfree(kv); + kv = next; + } +} + +// HACK: later versions of L4D2 show an annoying dialog on every plugin_load. +// We can suppress this by catching the message string that's passed from +// engine.dll to gameui.dll through KeyValuesSystem in vstdlib.dll and just +// replacing it with some other arbitrary garbage string. This makes gameui fail +// to match the message and thus do nothing. :) +static GetStringForSymbol_func orig_GetStringForSymbol = 0; +static const char *VCALLCONV hook_GetStringForSymbol(void *this, int s) { + const char *ret = orig_GetStringForSymbol(this, s); + if (!strcmp(ret, "OnClientPluginWarning")) ret = "sstBlockedThisEvent"; + return ret; +} + +INIT { + kvs = KeyValuesSystem(); + // NOTE: this is technically redundant for early versions but I CBA writing + // a version check; it's easier to just do this unilaterally. + if (GAMETYPE_MATCHES(L4D2x)) { + void **kvsvt = mem_loadptr(kvs); + if (!os_mprot(kvsvt + vtidx_GetStringForSymbol, sizeof(void *), + PAGE_READWRITE)) { + errmsg_warnx("couldn't make KeyValuesSystem vtable writable"); + errmsg_note("won't be able to prevent any nag messages"); + } + else { + orig_GetStringForSymbol = (GetStringForSymbol_func)hook_vtable( + kvsvt, vtidx_GetStringForSymbol, + (void *)hook_GetStringForSymbol); + } + } + return true; +} + +END { + if (orig_GetStringForSymbol) { + unhook_vtable(*(void ***)kvs, 4, (void *)orig_GetStringForSymbol); + } +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/kvsys.h b/src/kvsys.h new file mode 100644 index 0000000..0c8217d --- /dev/null +++ b/src/kvsys.h @@ -0,0 +1,59 @@ +/* + * Copyright © 2023 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. + */ + +#ifndef INC_KVSYS_H +#define INC_KVSYS_H + +#include "intdefs.h" + +struct KeyValues { + int keyname; + char *strval; + ushort *wstrval; + union { + int ival; + float fval; + void *pval; + }; + char datatype; + bool hasescapes; + bool evalcond; + //char unused; + struct KeyValues *next, *child, *chain; +}; + +/* Wraps the engine IKeyValuesSystem::GetStringForSymbol() call. */ +const char *kvsys_symtostr(int sym); + +/* Wraps the engine IKeyValuesSystem::GetSymbolForString() call. */ +int kvsys_strtosym(const char *s); + +/* Finds a subkey based on its interned name (via kvsys_strtosym() above) */ +struct KeyValues *kvsys_getsubkey(struct KeyValues *kv, int sym); + +/* + * Gets the string value of the KV object, or null if it doesn't have one. + * IMPORTANT: currently does not automatically coerce types like the engine + * does. This can be added later if actually required. + */ +const char *kvsys_getstrval(struct KeyValues *kv); + +/* Free a KV object and all its subkeys. */ +void kvsys_free(struct KeyValues *kv); + +#endif + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/l4d2vote.c b/src/l4d2vote.c deleted file mode 100644 index 06c61b8..0000000 --- a/src/l4d2vote.c +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br> - * - * 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 "con_.h" -#include "engineapi.h" -#include "ent.h" -#include "errmsg.h" -#include "feature.h" -#include "gamedata.h" -#include "gametype.h" -#include "intdefs.h" -#include "mem.h" -#include "x86.h" -#include "x86util.h" - -FEATURE("Left 4 Dead 2 vote cooldown resetting") -REQUIRE_GAMEDATA(vtidx_Spawn) - -static void **votecontroller = 0; -static int off_callerrecords = 0; - -// Note: the vote callers vector contains these as elements. We don't currently -// do anything with the structure, but keeping it here for reference. -/*struct CallerRecord { - u32 steamid_trunc; - float last_time; - int votes_passed; - int votes_failed; - int last_issueidx; - bool last_passed; -};*/ - -DEF_CCMD_HERE_UNREG(sst_l4d2_vote_cooldown_reset, - "Reset vote cooldown for all players", CON_CHEAT) { - if (!*votecontroller) { - con_warn("vote controller not initialised\n"); - return; - } - // Basically equivalent to CUtlVector::RemoveAll. The elements don't need - // to be destructed. This state is equivalent to when no one has voted yet - struct CUtlVector *recordvector = mem_offset(*votecontroller, - off_callerrecords); - recordvector->sz = 0; -} - -PREINIT { - // note: L4D1 has sv_vote_creation_timer but it doesn't actually do anything - return GAMETYPE_MATCHES(L4D2) && !!con_findvar("sv_vote_creation_timer"); -} - -static inline bool find_votecontroller(con_cmdcbv1 listissues_cb) { - const uchar *insns = (const uchar *)listissues_cb; -#ifdef _WIN32 - // The "listissues" command calls CVoteController::ListIssues, loading - // g_voteController into ECX - for (const uchar *p = insns; p - insns < 32;) { - if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) { - votecontroller = mem_loadptr(p + 2); - return true; - } - NEXT_INSN(p, "g_voteController variable"); - } -#else -#warning TODO(linux): this will be different -#endif - return false; -} - -// This finds the caller record vector using a pointer to the -// CVoteController::Spawn function -static inline bool find_votecallers(void *votectrlspawn) { - const uchar *insns = (const uchar *)votectrlspawn; - for (const uchar *p = insns; p - insns < 64;) { - // Unsure what the member on this offset actually is (the game seems to - // want it to be set to 0 to allow votes to happen), but the vector we - // want seems to consistently be 8 bytes after whatever this is - // "mov dword ptr [<reg> + off], 0", mod == 0b11 - if (p[0] == X86_MOVMIW && (p[1] & 0xC0) == 0x80 && - mem_load32(p + 6) == 0) { - off_callerrecords = mem_load32(p + 2) + 8; - return true; - } - NEXT_INSN(p, "vote caller record vector"); - } - return false; -} - -INIT { - struct con_cmd *cmd_listissues = con_findcmd("listissues"); - if (!cmd_listissues) { - errmsg_errorx("couldn't find \"listissues\" command"); - return false; - } - con_cmdcbv1 listissues_cb = con_getcmdcbv1(cmd_listissues); - if (!find_votecontroller(listissues_cb)) { - errmsg_errorx("couldn't find vote controller instance"); - return false; - } - - // g_voteController may have not been initialized yet so we get the vtable - // from the ent factory - const struct CEntityFactory *factory = ent_getfactory("vote_controller"); - if (!factory) { - errmsg_errorx("couldn't find vote controller entity factory"); - return false; - } - void **vtable = ent_findvtable(factory, "CVoteController"); - if (!vtable) { - errmsg_errorx("couldn't find CVoteController vtable"); - return false; - } - if (!find_votecallers(vtable[vtidx_Spawn])) { - errmsg_errorx("couldn't find vote callers vector offset"); - return false; - } - - con_reg(sst_l4d2_vote_cooldown_reset); - return true; -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/l4dmm.c b/src/l4dmm.c new file mode 100644 index 0000000..8394038 --- /dev/null +++ b/src/l4dmm.c @@ -0,0 +1,134 @@ +/* + * Copyright © 2023 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 <string.h> + +#include "engineapi.h" +#include "errmsg.h" +#include "feature.h" +#include "gamedata.h" +#include "gametype.h" +#include "kvsys.h" +#include "mem.h" +#include "os.h" +#include "vcall.h" + +FEATURE() +REQUIRE(kvsys) +REQUIRE_GAMEDATA(vtidx_GetMatchNetworkMsgController) +REQUIRE_GAMEDATA(vtidx_GetActiveGameServerDetails) + +DECL_VFUNC_DYN(void *, GetMatchNetworkMsgController) +DECL_VFUNC_DYN(struct KeyValues *, GetActiveGameServerDetails, + struct KeyValues *) + +// Old L4D1 uses a heavily modified version of the CMatchmaking in Source 2007. +// None of it is publicly documented or well-understood but I was able to figure +// out that this random function does something *close enough* to what we want. +struct contextval { + const char *name; + int _unknown[8]; + const char *val; + /* other stuff unknown */ +}; +DECL_VFUNC(struct contextval *, unknown_contextlookup, 67, const char *) + +static void *matchfwk; +static union { // space saving + struct { int sym_game, sym_campaign; }; // "game/campaign" KV lookup + void *oldmmiface; // old L4D1 interface +} U; +#define oldmmiface U.oldmmiface +#define sym_game U.sym_game +#define sym_campaign U.sym_campaign +static char campaignbuf[32]; + +const char *l4dmm_curcampaign(void) { +#ifdef _WIN32 + if (!matchfwk) { // we must have oldmmiface, then + struct contextval *ctxt = unknown_contextlookup(oldmmiface, + "CONTEXT_L4D_CAMPAIGN"); + if (ctxt) { + // HACK: since this context symbol stuff was the best that was found + // for this old MM interface, just map things back to their names + // manually. bit stupid, but it gets the (rather difficult) job done + if (strncmp(ctxt->val, "CONTEXT_L4D_CAMPAIGN_", 21)) return 0; + if (!strcmp(ctxt->val + 21, "APARTMENTS")) return "Hospital"; + if (!strcmp(ctxt->val + 21, "CAVES")) return "SmallTown"; + if (!strcmp(ctxt->val + 21, "GREENHOUSE")) return "Airport"; + if (!strcmp(ctxt->val + 21, "HILLTOP")) return "Farm"; + } + return 0; + } +#endif + void *ctrlr = GetMatchNetworkMsgController(matchfwk); + struct KeyValues *kv = GetActiveGameServerDetails(ctrlr, 0); + if (!kv) return 0; // not in server, probably + const char *ret = 0; + struct KeyValues *subkey = kvsys_getsubkey(kv, sym_game); + if (subkey) subkey = kvsys_getsubkey(subkey, sym_campaign); + if (subkey) ret = kvsys_getstrval(subkey); + if (ret) { + // ugh, we have to free all the memory allocated by the engine, so copy + // this glorified global state to a buffer so the caller doesn't have to + // deal with freeing. this necessitates a length cap but it's hopefully + // reasonable... + int len = strlen(ret); + if (len > sizeof(campaignbuf) - 1) ret = 0; + else ret = memcpy(campaignbuf, ret, len + 1); + } + kvsys_free(kv); + return ret; +} + +INIT { + // ugh, we NEED to centralise the 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 + if (mmlib) { + ifacefactory factory = (ifacefactory)os_dlsym(mmlib, "CreateInterface"); + if (!factory) { + errmsg_errordl("couldn't get matchmaking interface factory"); + return false; + } + matchfwk = factory("MATCHFRAMEWORK_001", 0); + if (!matchfwk) { + errmsg_errorx("couldn't get IMatchFramework interface"); + return false; + } + sym_game = kvsys_strtosym("game"); + sym_campaign = kvsys_strtosym("campaign"); + } + else { +#ifdef _WIN32 + oldmmiface = factory_engine("VENGINE_MATCHMAKING_VERSION001", 0); + if (!oldmmiface) { + errmsg_errorx("couldn't get IMatchmaking interface"); + return false; + } +#else // Linux L4D1 has always used the separate matchmaking library + errmsg_errordl("couldn't get matchmaking library"); + return false; +#endif + } + return true; +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/l4dmm.h b/src/l4dmm.h new file mode 100644 index 0000000..2897e17 --- /dev/null +++ b/src/l4dmm.h @@ -0,0 +1,32 @@ +/* + * Copyright © 2023 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. + */ + +#ifndef INC_L4DMM_H +#define INC_L4DMM_H + +/* + * Returns the ID of the current campaign, like L4D2C2 (L4D2) or Farm (L4D1). + * Copies to an internal buffer if required, so the caller is not required to + * manage memory. + * + * Returns null if no map is loaded (or the relevant metadata is somehow + * missing). + */ +const char *l4dmm_curcampaign(void); + +#endif + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/l4dreset.c b/src/l4dreset.c new file mode 100644 index 0000000..4bffa3c --- /dev/null +++ b/src/l4dreset.c @@ -0,0 +1,243 @@ +/* + * Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br> + * Copyright © 2023 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 <string.h> + +#include "con_.h" +#include "engineapi.h" +#include "ent.h" +#include "errmsg.h" +#include "feature.h" +#include "gamedata.h" +#include "gametype.h" +#include "intdefs.h" +#include "l4dmm.h" +#include "mem.h" +#include "vcall.h" +#include "x86.h" +#include "x86util.h" + +#ifdef _WIN32 +#define strcasecmp _stricmp +#endif + +FEATURE("Left 4 Dead quick resetting") +REQUIRE(ent) +REQUIRE(l4dmm) + +static void **votecontroller; +static int off_callerrecords = -1; +static int off_voteissues; + +// Note: the vote callers vector contains these as elements. We don't currently +// do anything with the structure, but keeping it here for reference. +/*struct CallerRecord { + u32 steamid_trunc; + float last_time; + int votes_passed; + int votes_failed; + int last_issueidx; + bool last_passed; +};*/ + +// XXX: duping this again here... what makes sense to tidy this up? +#ifdef _WIN32 +#define NVDTOR 1 +#else +#define NVDTOR 2 +#endif + +struct CVoteIssue; +DECL_VFUNC(const char *, SetIssueDetails, 1 + NVDTOR, const char *) +DECL_VFUNC(const char *, GetDisplayString, 8 + NVDTOR) +DECL_VFUNC(const char *, ExecuteCommand, 9 + NVDTOR) + +static struct CVoteIssue *getissue(const char *textkey) { + struct CUtlVector *issuevec = mem_offset(*votecontroller, off_voteissues); + struct CVoteIssue **issues = issuevec->m.mem; + for (int i = 0; /*i < issuevec->sz*/; ++i) { // key MUST be valid! + if (!strcmp(GetDisplayString(issues[i]), textkey)) return issues[i]; + } +} + +static void reset(void) { + // reset the vote cooldowns if possible (will skip L4D1). only necessary on + // versions >2045 and on map 1, but it's easiest to do unconditionally + if (off_callerrecords != -1) { + // Basically equivalent to CUtlVector::RemoveAll. The elements have no + // destructors to call. The resulting state is as if nobody has voted. + struct CUtlVector *recordvector = mem_offset(*votecontroller, + off_callerrecords); + recordvector->sz = 0; + } + struct CVoteIssue *issue = getissue("#L4D_vote_restart_game"); + ExecuteCommand(issue); +} + +static void change(const char *missionid) { + struct CVoteIssue *issue = getissue("#L4D_vote_mission_change"); + SetIssueDetails(issue, missionid); // will just nop if invalid + ExecuteCommand(issue); +} + +DEF_CCMD_HERE_UNREG(sst_l4d_quickreset, + "Reset (or switch) campaign and clear all vote cooldowns", 0) { + if (cmd->argc > 2) { + con_warn("usage: sst_l4d_quickreset [campaignid]\n"); + return; + } + if (!*votecontroller) { + con_warn("not hosting a server\n"); + return; + } + if (cmd->argc == 2) { + const char *cur = l4dmm_curcampaign(); + if (!cur || strcasecmp(cur, cmd->argv[1])) { + change(cmd->argv[1]); + return; + } + } + reset(); +} + +PREINIT { return GAMETYPE_MATCHES(L4D); } + +// This finds the g_voteController variable using the listissues callback, and +// returns a pointer to the rest of the bytes for find_voteissues() below +static inline const uchar *find_votecontroller(con_cmdcbv1 listissues_cb) { + const uchar *insns = (const uchar *)listissues_cb; +#ifdef _WIN32 + // The "listissues" command calls CVoteController::ListIssues, loading + // g_voteController into ECX + for (const uchar *p = insns; p - insns < 32;) { + if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) { + votecontroller = mem_loadptr(p + 2); + return p; + } + NEXT_INSN(p, "g_voteController variable"); + } +#else +#warning TODO(linux): this will be different +#endif + return 0; +} + +// This finds ListIssues() using the instruction pointer returned by +// find_votecontroller() above, and then uses that to find the vote issue list. +static inline bool find_voteissues(const uchar *insns) { +#ifdef _WIN32 + for (const uchar *p = insns; p - insns < 16;) { + // Look for the last call before the ret - that has to be ListIssues() + if (p[0] == X86_CALL && p[5] == X86_RET) { + insns = p + 5 + mem_loadoffset(p + 1); + goto ok; + } + NEXT_INSN(p, "ListIssues call"); + } + return false; +ok: for (const uchar *p = insns; p - insns < 96;) { + // There's a virtual call on each actual CVoteIssue in the loop over the + // list. That entails putting the issue pointer in ECX, which involves + // loading that pointer from the vector, which exists at an offset from + // `this`, meaning we can find the offset from the mov into ECX. + if (p[0] == X86_MOVRMW && (p[1] & 0xF8) == 0x88) { + int off = mem_loadoffset(p + 2); + if (off > 800) { // sanity check: offset is always fairly high + off_voteissues = off; + return true; + } + } + // Further complication: at least in 2045 there's a short jmp over some + // invalid instruction bytes. I guess there's no reason to ever expect + // something interesting after an unconditional jmp, so just follow it. + if (p[0] == X86_JMPI8) { + p += 2 + ((s8 *)p)[1]; + continue; + } + NEXT_INSN(p, "offset to vote issue vector"); + } +#else +#warning TODO(linux): and also this +#endif + return false; +} + +// This finds the caller record vector using a pointer to the +// CVoteController::Spawn function +static inline bool find_votecallers(void *votectrlspawn) { +#ifdef _WIN32 + const uchar *insns = (const uchar *)votectrlspawn; + for (const uchar *p = insns; p - insns < 64;) { + // Unsure what the member on this offset actually is (the game seems to + // want it to be set to 0 to allow votes to happen), but the vector we + // want seems to consistently be 8 bytes after whatever this is + // "mov dword ptr [<reg> + off], 0", mod == 0b11 + if (p[0] == X86_MOVMIW && (p[1] & 0xC0) == 0x80 && + mem_load32(p + 6) == 0) { + off_callerrecords = mem_load32(p + 2) + 8; + return true; + } + NEXT_INSN(p, "offset to vote caller record vector"); + } +#else +#warning TODO(linux): this too +#endif + return false; +} + +INIT { + struct con_cmd *cmd_listissues = con_findcmd("listissues"); + if (!cmd_listissues) { + errmsg_errorx("couldn't find \"listissues\" command"); + return false; + } + con_cmdcbv1 listissues_cb = con_getcmdcbv1(cmd_listissues); + const uchar *nextinsns = find_votecontroller(listissues_cb); + if (!nextinsns) { + errmsg_errorx("couldn't find vote controller variable"); + return false; + } + if (!find_voteissues(nextinsns)) { + errmsg_errorx("couldn't find vote issues list offset\n"); + return false; + } + // only bother with vote cooldown stuff for L4D2, since all versions of L4D1 + // have unlimited votes anyway. NOTE: assuming L4D2 always has Spawn in + // gamedata (which has no reason to stop being true...) + if (GAMETYPE_MATCHES(L4D2)) { + // g_voteController may have not been initialized yet so we get the + // vtable from the ent factory + const struct CEntityFactory *factory = ent_getfactory("vote_controller"); + if (!factory) { + errmsg_errorx("couldn't find vote controller entity factory"); + goto nocd; + } + void **vtable = ent_findvtable(factory, "CVoteController"); + if (!vtable) { + errmsg_errorx("couldn't find CVoteController vtable"); + goto nocd; + } + if (!find_votecallers(vtable[vtidx_Spawn])) { + errmsg_errorx("couldn't find vote callers list offset"); +nocd: errmsg_note("resetting a first map will not clear vote cooldowns"); + } + } + con_reg(sst_l4d_quickreset); + return true; +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/l4dwarp.c b/src/l4dwarp.c index 5540763..75c762c 100644 --- a/src/l4dwarp.c +++ b/src/l4dwarp.c @@ -29,6 +29,7 @@ #include "vcall.h" FEATURE("Left 4 Dead warp testing") +REQUIRE(ent) REQUIRE_GAMEDATA(off_entpos) REQUIRE_GAMEDATA(off_eyeang) REQUIRE_GAMEDATA(vtidx_Teleport) diff --git a/src/rinput.c b/src/rinput.c index 53c16e3..6b6d4d7 100644 --- a/src/rinput.c +++ b/src/rinput.c @@ -51,12 +51,12 @@ FEATURE("scalable raw mouse input") #define USAGE_MOUSE 2 static int cx, cy, rx = 0, ry = 0; // cursor xy, remainder xy -static union { // cheeky space saving +static union { // space saving void *inwin; void **vtable_insys; -} u1; -#define inwin u1.inwin -#define vtable_insys u1.vtable_insys +} U; +#define inwin U.inwin +#define vtable_insys U.vtable_insys DEF_CVAR_UNREG(m_rawinput, "Use Raw Input for mouse input (SST reimplementation)", 0, CON_ARCHIVE | CON_HIDDEN) @@ -143,23 +143,6 @@ DEF_CCMD_HERE(sst_printversion, "Display plugin version information", 0) { con_msg("v" VERSION "\n"); } -// HACK: later versions of L4D2 show an annoying dialog on every plugin_load. -// We can suppress this by catching the message string that's passed from -// engine.dll to gameui.dll through KeyValuesSystem in vstdlib.dll and just -// replacing it with some other arbitrary garbage string. This makes gameui fail -// to match the message and thus do nothing. :) -static void **kvsvt; -typedef const char *(*VCALLCONV GetStringForSymbol_func)(void *this, int s); -static GetStringForSymbol_func orig_GetStringForSymbol = 0; -static const char *VCALLCONV GetStringForSymbol_hook(void *this, int s) { - const char *ret = orig_GetStringForSymbol(this, s); - if (!strcmp(ret, "OnClientPluginWarning")) ret = "sstBlockedThisEvent"; - return ret; -} - -// vstdlib symbol, only currently used in l4d2 but exists everywhere so oh well -IMPORT void *KeyValuesSystem(void); - // 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) {} @@ -317,21 +300,6 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { *p++ = (void *)&nop_p_v; // OnEdictAllocated *p = (void *)&nop_p_v; // OnEdictFreed - // NOTE: this is technically redundant for early versions but I CBA writing - // a version check; it's easier to just do this unilaterally. - if (GAMETYPE_MATCHES(L4D2x)) { - void *kvs = KeyValuesSystem(); - kvsvt = *(void ***)kvs; - if (!os_mprot(kvsvt + 4, sizeof(void *), PAGE_READWRITE)) { - errmsg_warnx("couldn't make KeyValuesSystem vtable writable"); - errmsg_note("won't be able to prevent any nag messages"); - } - else { - orig_GetStringForSymbol = (GetStringForSymbol_func)hook_vtable( - kvsvt, 4, (void *)GetStringForSymbol_hook); - } - } - if (!deferinit()) do_featureinit(); return true; } @@ -378,10 +346,6 @@ static void do_unload(void) { if (clientlib) dlclose(clientlib); #endif con_disconnect(); - - if (orig_GetStringForSymbol) { - unhook_vtable(kvsvt, 4, (void *)orig_GetStringForSymbol); - } } static bool VCALLCONV Load(void *this, ifacefactory enginef, |