diff options
author | Michael Smith <mikesmiffy128@gmail.com> | 2023-06-02 17:03:47 +0100 |
---|---|---|
committer | Michael Smith <mikesmiffy128@gmail.com> | 2023-06-03 13:25:12 +0100 |
commit | 602a18977d500ad068fd63fbedcafb630c29ee72 (patch) | |
tree | 84ef367d7b0dd2870520c3cdb141e41ec2ca212b | |
parent | 2ba71f27c46dc38b76e932b1b1967d96a9b9f107 (diff) |
Adapt vote reset code into fast campaign resetting
This is kind of a breaking change but the other code was obviously never
released or relied on by anyone - it will be pushed at the same time as
this in fact. It still seems worth having the original committed
separately to show the progression of development of the feature,
however. Technically the standalone vote cooldown resetting could also
be added back if ever desired however there doesn't seem to be that much
of a use case for that at the moment.
This feature ought to be a lot more convenient now as it allows for
resetting back to a set starting point no matter where the player is in
a run. It isn't universally useful as All Campaigns Legacy solo runs
require switching to a different type of server and Main Campaigns co-op
runs require restarting the game after Swamp Fever to work around the
god mode bug, however it is still useful in a good few situations.
Unfortunately this turned out to be pretty complex to implement, first
requiring a bunch of interop with valve's rather wacky KeyValues stuff,
and then requiring a bunch of especially difficult reverse engineering
of L4D1 v1.0.0.5 because it doesn't use said KeyValues stuff and does
something else completely different instead.
A side effect of all this work is that the nag removal hack is now part
of the KeyValues stuff in kvsys.c, which is kind of a comfier place for
it than just kind of dumped in the middle of sst.c.
-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, |