summaryrefslogtreecommitdiffhomepage
path: root/src/l4dreset.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/l4dreset.c')
-rw-r--r--src/l4dreset.c243
1 files changed, 243 insertions, 0 deletions
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