summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rwxr-xr-xcompile7
-rw-r--r--compile.bat6
-rw-r--r--gamedata/matchmaking.kv11
-rw-r--r--src/kvsys.c98
-rw-r--r--src/kvsys.h59
-rw-r--r--src/l4d2vote.c134
-rw-r--r--src/l4dmm.c134
-rw-r--r--src/l4dmm.h32
-rw-r--r--src/l4dreset.c243
-rw-r--r--src/l4dwarp.c1
-rw-r--r--src/rinput.c8
-rw-r--r--src/sst.c36
12 files changed, 591 insertions, 178 deletions
diff --git a/compile b/compile
index a3d07a8..fc88c10 100755
--- a/compile
+++ b/compile
@@ -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)
diff --git a/src/sst.c b/src/sst.c
index 5d21f06..a0f926d 100644
--- a/src/sst.c
+++ b/src/sst.c
@@ -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,