summaryrefslogtreecommitdiffhomepage
path: root/src/l4dreset.c
diff options
context:
space:
mode:
authorMichael Smith <mikesmiffy128@gmail.com>2023-11-20 20:22:55 +0000
committerMichael Smith <mikesmiffy128@gmail.com>2023-12-04 19:30:09 +0000
commit324ebec00666aeaa45828712fcc27b70ef86350a (patch)
treeb4353c23d9237c3acd2bac82b9b3f923e52244d5 /src/l4dreset.c
parent44d805746e27c58f906ba73f541abcb363323a75 (diff)
Add cutscene skipping to L4D quick reset
Also done with quite a lot of RE help from bill - thanks again!
Diffstat (limited to 'src/l4dreset.c')
-rw-r--r--src/l4dreset.c215
1 files changed, 201 insertions, 14 deletions
diff --git a/src/l4dreset.c b/src/l4dreset.c
index 2245a16..2477b8b 100644
--- a/src/l4dreset.c
+++ b/src/l4dreset.c
@@ -21,12 +21,16 @@
#include "engineapi.h"
#include "ent.h"
#include "errmsg.h"
+#include "event.h"
+#include "fastfwd.h"
#include "feature.h"
#include "gamedata.h"
#include "gametype.h"
+#include "hook.h"
#include "intdefs.h"
#include "l4dmm.h"
#include "mem.h"
+#include "sst.h"
#include "vcall.h"
#include "x86.h"
#include "x86util.h"
@@ -37,12 +41,19 @@
FEATURE("Left 4 Dead quick resetting")
REQUIRE(ent)
+REQUIRE(fastfwd)
REQUIRE(l4dmm)
+REQUIRE_GLOBAL(srvdll)
+REQUIRE_GAMEDATA(vtidx_GameFrame) // note: for L4D1 only, always defined anyway
+REQUIRE_GAMEDATA(vtidx_GameShutdown)
+REQUIRE_GAMEDATA(vtidx_OnGameplayStart)
static void **votecontroller;
static int off_callerrecords = -1;
static int off_voteissues;
+static void *director; // "TheDirector" server global
+
// Note: the vote callers vector contains these as elements. We don't currently
// do anything with the structure, but we're keeping it here for reference.
/*struct CallerRecord {
@@ -74,7 +85,7 @@ static struct CVoteIssue *getissue(const char *textkey) {
}
}
-static void reset(void) {
+static inline 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) {
@@ -84,16 +95,90 @@ static void reset(void) {
off_callerrecords);
recordvector->sz = 0;
}
- struct CVoteIssue *issue = getissue("#L4D_vote_restart_game");
- ExecuteCommand(issue);
+ ExecuteCommand(getissue("#L4D_vote_restart_game"));
}
-static void change(const char *missionid) {
+static inline void change(const char *missionid) {
struct CVoteIssue *issue = getissue("#L4D_vote_mission_change");
SetIssueDetails(issue, missionid); // will just nop if invalid
ExecuteCommand(issue);
}
+static short ffdelay = 0, ffamt;
+static bool mapchanging = false;
+
+HANDLE_EVENT(Tick, bool simulating) {
+ if (!mapchanging && ffdelay && !--ffdelay) fastfwd(ffamt, 30);
+}
+
+typedef void (*VCALLCONV OnGameplayStart_func)(void *this);
+static OnGameplayStart_func orig_OnGameplayStart;
+static void VCALLCONV hook_OnGameplayStart(void *this) {
+ orig_OnGameplayStart(this);
+ if (mapchanging) reset(); // prevent bots walking around. note ffdelay is 45
+ mapchanging = false; // resume countdown!
+}
+// Simply reuse the above for L4D1, since the calling ABI is the exact same!
+#define UnfreezeTeam_func OnGameplayStart_func
+#define UnfreezeTeam OnGameplayStart
+#define orig_UnfreezeTeam orig_OnGameplayStart
+#define hook_UnfreezeTeam hook_OnGameplayStart
+
+static inline int getffamt(const char *campaign) {
+ // TODO(compat): add popular custom campaigns too:
+ // - dark blood 2
+ // - grey scale
+ // - left 4 mario
+ // - ravenholm
+ // - warcelona
+ // - tour of terror
+ // - dam it
+ // - carried off
+ if (GAMETYPE_MATCHES(L4D1)) {
+ if (!strcmp(campaign, "Hospital")) return 9; // No Mercy
+ if (!strcmp(campaign, "SmallTown")) return 12; // Death Toll
+ if (!strcmp(campaign, "Airport")) return 12; // Dead Air
+ if (!strcmp(campaign, "Farm")) return 15; // Blood Harvest
+ if (!strcmp(campaign, "River")) return 15; // The Sacrifice
+ if (!strcmp(campaign, "Garage")) return 8; // Crash Course
+ }
+ else /* L4D2 */ {
+ if (!strncmp(campaign, "L4D2C", 5)) {
+ int ret;
+ switch (campaign[5]) {
+ case '1':
+ switch (campaign[6]) {
+ case '\0': return 13; // Dead Center
+ case '0': ret = 12; break; // Death Toll
+ case '1': ret = 13; break; // Dead Air
+ case '2': ret = 16; break; // Blood Harvest
+ case '3': ret = 18; break; // Cold Stream
+ case '4': ret = 16; break; // The Last Stand
+ default: return 0;
+ }
+ if (campaign[7]) return 0;
+ return ret;
+ case '2': ret = 12; break; // Dark Carnival
+ case '3': ret = 19; break; // Swamp Fever
+ case '4': ret = 8; break; // Hard Rain
+ case '5': ret = 13; break; // The Parish
+ case '6': ret = 10; break; // The Passing
+ case '7': ret = 15; break; // The Sacrifice
+ case '8': ret = 9; break; // No Mercy
+ case '9': ret = 8; break; // Crash Course
+ default: return 0;
+ }
+ if (campaign[6]) return 0;
+ return ret;
+ }
+ }
+ return 0; // if unknown, just don't skip, I guess.
+}
+
+DEF_CVAR_UNREG(sst_l4d_quickreset_fastfwd,
+ "Fast-forward through cutscenes when quick-resetting", 1,
+ CON_ARCHIVE | CON_HIDDEN)
+
DEF_CCMD_HERE_UNREG(sst_l4d_quickreset,
"Reset (or switch) campaign and clear all vote cooldowns", 0) {
if (cmd->argc > 2) {
@@ -104,17 +189,29 @@ DEF_CCMD_HERE_UNREG(sst_l4d_quickreset,
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;
- }
+ const char *campaign = l4dmm_curcampaign();
+ if (cmd->argc == 2 && (!campaign || strcasecmp(campaign, cmd->argv[1]))) {
+ change(cmd->argv[1]);
+ campaign = cmd->argv[1];
+ mapchanging = true;
+ }
+ else {
+ reset();
+ // same-map reset is delayed by about a second - save that time here
+ // also, set mapchanging back to false in case it got stuck somehow
+ if (!(mapchanging = !l4dmm_firstmap())) fastfwd(0.8, 10);
+ }
+ if (campaign && con_getvari(sst_l4d_quickreset_fastfwd) &&
+ (ffamt = getffamt(campaign))) {
+ ffdelay = 45; // 1.5s
}
- reset();
}
-PREINIT { return GAMETYPE_MATCHES(L4D); }
+PREINIT {
+ if (!GAMETYPE_MATCHES(L4D)) return false;
+ con_reg(sst_l4d_quickreset_fastfwd);
+ return true;
+}
// Note: this returns a pointer to subsequent bytes for find_voteissues() below
static inline const uchar *find_votecontroller(con_cmdcbv1 listissues_cb) {
@@ -191,6 +288,52 @@ static inline bool find_votecallers(void *votectrlspawn) {
return false;
}
+#ifdef _WIN32
+static inline bool find_TheDirector(void *GameShutdown) {
+ // in 2045, literally the first instruction of this function is loading
+ // TheDirector into ECX. although, do the usual search in case moves a bit.
+ const uchar *insns = (const uchar *)GameShutdown;
+ for (const uchar *p = insns; p - insns < 24;) {
+ if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) {
+ void **indirect = mem_loadptr(p + 2);
+ director = *indirect;
+ return true;
+ }
+ NEXT_INSN(p, "load of TheDirector");
+ }
+ return false;
+}
+#endif
+
+static inline bool find_UnfreezeTeam(void *GameFrame) { // note: L4D1 only
+ // CServerGameDLL::GameFrame() loads TheDirector into ECX and then calls
+ // Director::Update()
+ const uchar *insns = (const uchar *)GameFrame, *p = insns;
+ while (p - insns < 192) {
+ if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5) &&
+ mem_loadptr(mem_loadptr(p + 2)) == director &&
+ p[6] == X86_CALL) {
+ p += 11 + mem_loadoffset(p + 7);
+ insns = p;
+ goto ok;
+ }
+ NEXT_INSN(p, "Director::Update call");
+ }
+ return false;
+ok: // Director::Update calls UnfreezeTeam after the first jmp instruction
+ while (p - insns < 96) {
+ // jz XXX; mov ecx, <reg>; call Director::UnfreezeTeam
+ if (p[0] == X86_JZ && p[2] == X86_MOVRMW && (p[3] & 0xF8) == 0xC8 &&
+ p[4] == X86_CALL) {
+ p += 9 + mem_loadoffset(p + 5);
+ orig_UnfreezeTeam = (UnfreezeTeam_func)p;
+ return true;
+ }
+ NEXT_INSN(p, "Director::UnfreezeTeam call");
+ }
+ return false;
+ }
+
INIT {
struct con_cmd *cmd_listissues = con_findcmd("listissues");
if (!cmd_listissues) {
@@ -207,8 +350,41 @@ INIT {
errmsg_errorx("couldn't find vote issues list offset\n");
return false;
}
+ void **vtable;
+#ifdef _WIN32
+ void *GameShutdown = (*(void ***)srvdll)[vtidx_GameShutdown];
+ if (!find_TheDirector(GameShutdown)) {
+ errmsg_errorx("couldn't find TheDirector variable");
+ return false;
+ }
+#else
+#warning TODO(linux): should be able to just dlsym(server, "TheDirector")
+ return false;
+#endif
+#ifdef _WIN32 // L4D1 has no Linux build, no need to check whether L4D2
+ if (GAMETYPE_MATCHES(L4D2)) {
+#endif
+ vtable = mem_loadptr(director);
+ if (!os_mprot(vtable + vtidx_OnGameplayStart, sizeof(*vtable),
+ PAGE_READWRITE)) {
+ errmsg_errorsys("couldn't make virtual table writable");
+ return false;
+ }
+ orig_OnGameplayStart = (OnGameplayStart_func)hook_vtable(vtable,
+ vtidx_OnGameplayStart, (void *)&hook_OnGameplayStart);
+#ifdef _WIN32 // L4D1 has no Linux build!
+ }
+ else /* L4D1 */ {
+ void *GameFrame = (*(void ***)srvdll)[vtidx_GameFrame];
+ if (!find_UnfreezeTeam(GameFrame)) {
+ errmsg_errorx("couldn't find UnfreezeTeam function");
+ return false;
+ }
+ orig_UnfreezeTeam = (UnfreezeTeam_func)hook_inline(
+ (void *)orig_UnfreezeTeam, (void *)&hook_UnfreezeTeam);
+ }
+#endif
// Only try cooldown stuff for L4D2, since L4D1 always had unlimited votes.
- // NOTE: assuming L4D2 always has Spawn in gamedata (why wouldn't it?)
if (GAMETYPE_MATCHES(L4D2)) {
// g_voteController is invalid if not running a server so get the
// vtable by inspecting the ent factory code instead
@@ -217,7 +393,7 @@ INIT {
errmsg_errorx("couldn't find vote controller entity factory");
goto nocd;
}
- void **vtable = ent_findvtable(factory, "CVoteController");
+ vtable = ent_findvtable(factory, "CVoteController");
if (!vtable) {
errmsg_errorx("couldn't find CVoteController vtable");
goto nocd;
@@ -228,7 +404,18 @@ nocd: errmsg_note("resetting a first map will not clear vote cooldowns");
}
}
con_reg(sst_l4d_quickreset);
+ sst_l4d_quickreset_fastfwd->base.flags &= ~CON_HIDDEN;
return true;
}
+END {
+ if (GAMETYPE_MATCHES(L4D2)) {
+ unhook_vtable(mem_loadptr(director), vtidx_OnGameplayStart,
+ (void *)orig_OnGameplayStart);
+ }
+ else {
+ unhook_inline((void *)orig_UnfreezeTeam);
+ }
+}
+
// vi: sw=4 ts=4 noet tw=80 cc=80