From af370088fc178996a46ee508aaa1a2c46318bbf2 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 26 Dec 2025 23:12:11 +0000 Subject: Move to static trampolines and type-safe hooks For some reason the DLL got a tiny little bit bigger again but that's fine. This will make orig_ calls more efficient in the inline case, and also make it harder to screw up and hook the wrong thing by mistake. Self-explanatory-ish, apart from the fact it's a fairly large API change of course. And it relies on some more bonkers assembler directive hackery. But it works! The only complaint one might have is that the featsetup functions no longer take an explicit string which occasionally yields slightly less perfect error messages, but I've decided this isn't really a problem and makes the API nicer to use. It's a tradeoff, innit. --- src/l4dreset.c | 63 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) (limited to 'src/l4dreset.c') diff --git a/src/l4dreset.c b/src/l4dreset.c index dd59c60..25d3d10 100644 --- a/src/l4dreset.c +++ b/src/l4dreset.c @@ -235,10 +235,7 @@ HANDLE_EVENT(Tick, bool simulating) { } } -typedef void (*VCALLCONV OnGameplayStart_func)(struct CDirector *this); -static OnGameplayStart_func orig_OnGameplayStart; -static void VCALLCONV hook_OnGameplayStart(struct CDirector *this) { - orig_OnGameplayStart(this); +static void onstart() { if (nextmapnum) { // if we changed map more than 1 time, cancel the reset. this'll happen // if someone prematurely disconnects and then starts a new session. @@ -247,11 +244,21 @@ static void VCALLCONV hook_OnGameplayStart(struct CDirector *this) { } nextmapnum = 0; // resume countdown if there is one! otherwise do nothing. } -// 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 + +typedef void (*VCALLCONV OnGameplayStart_func)(struct CDirector *this); +DEF_VTABLE_HOOK(OnGameplayStart_func, OnGameplayStart) +static void VCALLCONV hookdest_OnGameplayStart(struct CDirector *this) { + orig_OnGameplayStart(this); + onstart(); +} +typedef void (*VCALLCONV UnfreezeTeam_func)(struct CDirector *this); +DEF_INLINE_HOOK(UnfreezeTeam_func, UnfreezeTeam) +// UnfreezeTeam is pretty much identical to OnGameplayStart except for using +// a direct orig_ call (static trampoline) rather than a function pointer. +static void VCALLCONV hookdest_UnfreezeTeam(struct CDirector *this) { + orig_UnfreezeTeam(this); + onstart(); +} static int getffidx(const char *campaign) { if (GAMETYPE_MATCHES(L4D1)) { @@ -471,7 +478,9 @@ static inline bool find_TheDirector(void *GameShutdown) { } #endif -static inline bool find_UnfreezeTeam(void *GameFrame) { // note: L4D1 only +// note: this function is for L4D1 only +static inline UnfreezeTeam_func find_UnfreezeTeam(void *GameFrame) { +#ifdef _WIN32 // CServerGameDLL::GameFrame() loads TheDirector into ECX and then calls // Director::Update() const uchar *insns = (const uchar *)GameFrame, *p = insns; @@ -485,20 +494,19 @@ static inline bool find_UnfreezeTeam(void *GameFrame) { // note: L4D1 only } NEXT_INSN(p, "Director::Update call"); } - return false; + return 0; ok: // Director::Update calls UnfreezeTeam after the first jmp instruction while (p - insns < 96) { // jz XXX; mov ecx, ; call Director::UnfreezeTeam if (p[0] == X86_JZ && p[2] == X86_MOVRMW && (p[3] & 0xF8) == 0xC8 && p[4] == X86_CALL) { - p += 9 + mem_loads32(p + 5); - orig_UnfreezeTeam = (UnfreezeTeam_func)p; - return true; + return (UnfreezeTeam_func)(p + 9 + mem_loads32(p + 5)); } NEXT_INSN(p, "Director::UnfreezeTeam call"); } - return false; - } +#endif + return 0; +} // XXX: duped def in democustom: should this belong somewhere else? DECL_VFUNC_DYN(struct VEngineClient, int, GetEngineBuildNumber) @@ -529,32 +537,28 @@ INIT { #warning TODO(linux): should be able to just dlsym(server, "TheDirector") return FEAT_INCOMPAT; #endif -#ifdef _WIN32 // L4D1 has no Linux build, no need to check whether L4D2 if (GAMETYPE_MATCHES(L4D2)) { -#endif vtable = director->vtable; if_cold (!os_mprot(vtable + vtidx_OnGameplayStart, sizeof(*vtable), PAGE_READWRITE)) { errmsg_errorsys("couldn't make virtual table writable"); return FEAT_FAIL; } - orig_OnGameplayStart = (OnGameplayStart_func)hook_vtable(vtable, - vtidx_OnGameplayStart, (void *)&hook_OnGameplayStart); -#ifdef _WIN32 // L4D1 has no Linux build! + hook_OnGameplayStart(vtable, vtidx_OnGameplayStart, + &hookdest_OnGameplayStart); } else /* L4D1 */ { void *GameFrame = srvdll->vtable[vtidx_GameFrame]; - if_cold (!find_UnfreezeTeam(GameFrame)) { + UnfreezeTeam_func UnfreezeTeam = find_UnfreezeTeam(GameFrame); + if_cold (!UnfreezeTeam) { errmsg_errorx("couldn't find UnfreezeTeam function"); return FEAT_INCOMPAT; } - struct hook_inline_featsetup_ret h = hook_inline_featsetup( - (void *)orig_UnfreezeTeam, (void **)&orig_UnfreezeTeam, - "UnfreezeTeam"); + struct hook_featsetup_ret_UnfreezeTeam h = + hook_featsetup_UnfreezeTeam( UnfreezeTeam); if_cold (h.err) return h.err; - hook_inline_commit(h.prologue, (void *)&hook_UnfreezeTeam); + hook_commit_UnfreezeTeam(h.hookpos, &hookdest_UnfreezeTeam); } -#endif // Only try cooldown stuff for L4D2, since L4D1 always had unlimited votes. if (GAMETYPE_MATCHES(L4D2)) { // g_voteController is invalid if not running a server so get the @@ -589,11 +593,10 @@ nocd: errmsg_note("resetting a first map will not clear vote cooldowns"); END { if (GAMETYPE_MATCHES(L4D2)) { - unhook_vtable(mem_loadptr(director), vtidx_OnGameplayStart, - (void *)orig_OnGameplayStart); + unhook_OnGameplayStart(mem_loadptr(director), vtidx_OnGameplayStart); } else { - unhook_inline((void *)orig_UnfreezeTeam); + unhook_UnfreezeTeam(); } } -- cgit v1.2.3-54-g00ecf