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,  | 
