aboutsummaryrefslogtreecommitdiff
path: root/src/l4d1democompat.c
blob: 1d4d1c9608aaccce9df6b032893a18eea942b628 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/*
 * Copyright © 2025 Hayden K <imaciidz@gmail.com>
 * Copyright © 2025 Willian Henrique <wsimanbrazil@yahoo.com.br>
 * Copyright © 2025 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 "accessor.h"
#include "con_.h"
#include "errmsg.h"
#include "feature.h"
#include "hook.h"
#include "intdefs.h"
#include "mem.h"
#include "sst.h"
#include "vcall.h"
#include "x86.h"
#include "x86util.h"

FEATURE("Left 4 Dead 1 demo file backwards compatibility")
GAMESPECIFIC(L4D1_1022plus)

// NOTE: not bothering to put this in gamedata since it's actually a constant.
// We could optimise the gamedata system further to constant-fold things with no
// leaves beyond the GAMESPECIFIC cutoff or whatever. But that sounds annoying.
#define off_CDemoFile_protocol 272
DEF_ACCESSORS(int, CDemoFile_protocol)

// L4D1 bumps the demo protocol version with every update to the game, which
// means whenever there is a security update, you cannot watch old demos. From
// minimal testing, it seems demos recorded on version 1022 and onwards are
// compatible with the latest version of the game, so this code lets us watch
// 1022+ demos on any later version of the game.

typedef int (*GetHostVersion_func)();
static GetHostVersion_func orig_GetHostVersion;

typedef void (*VCALLCONV ReadDemoHeader_func)(void *);
static ReadDemoHeader_func orig_ReadDemoHeader;

static inline bool find_ReadDemoHeader(con_cmdcb cb) {
	// Find the call to ReadDemoHeader in the listdemo callback
	const uchar *insns = (const uchar*)cb;
	for (const uchar *p = insns; p - insns < 192;) {
		if (p[0] == X86_LEA && p[1] == X86_MODRM(2, 1, 4) && p[2] == 0x24 &&
				p[7] == X86_CALL && p[12] == X86_LEA &&
				p[13] == X86_MODRM(2, 1, 4) && p[14] == 0x24) {
			orig_ReadDemoHeader =
					(ReadDemoHeader_func)(p + 12 + mem_loads32(p + 8));
			return true;
		}
		NEXT_INSN(p, "ReadDemoHeader");
	}
	return false;
}

static void *ReadDemoHeader_midpoint;

static inline bool find_midpoint() {
	uchar *insns = (uchar *)orig_ReadDemoHeader;
	for (uchar *p = insns; p - insns < 128;) {
		if (p[0] == X86_PUSHIW && p[5] == X86_PUSHEBX && p[6] == X86_CALL &&
				!memcmp(mem_loadptr(p + 1), "HL2DEMO", 7)) {
			ReadDemoHeader_midpoint = (p + 11);
			return true;
		}
		NEXT_INSN(p, "ReadDemoHeader hook midpoint");
	}
	return false;
}

static inline bool find_GetHostVersion() {
	uchar *insns = (uchar *)orig_ReadDemoHeader;
	int jzcnt = 0;
	for (uchar *p = insns; p - insns < 192;) {
		// GetHostVersion() is called right after the third JZ insn in
		// ReadDemoHeader()
		if (p[0] == X86_JZ && ++jzcnt == 3) {
			orig_GetHostVersion =
					(GetHostVersion_func)(p + 7 + mem_loads32(p + 3));
			return true;
		}
		NEXT_INSN(p, "GetHostVersion");
	}
	return false;
}

static int demoversion, gameversion;

static int hook_GetHostVersion() {
	// If the demo version is 1022 or later, and not newer than the version we
	// are currently using, then we spoof the game version to let the demo play.
	if (demoversion >= 1022 && demoversion <= gameversion) return demoversion;
	return gameversion;
}

static int *this_protocol;
static void VCALLCONV hook_ReadDemoHeader(void *this) {
	// The mid-function hook needs to get the protocol from `this`, but by that
	// point we won't be able to rely on the ECX register and/or any particular
	// stack spill layout. So... offset the pointer and stick it in a global.
	this_protocol = getptr_CDemoFile_protocol(this);
	orig_ReadDemoHeader(this);
}

#if defined(__clang__)
__attribute__((naked))
#elif defined(_MSC_VER)
#error Inadequate inline assembly syntax, use Clang instead.
#else
#error No way to do naked functions! We only support Clang at the moment.
#endif
static int hook_midpoint() {
	__asm__ volatile (
		"pushl %%eax\n"
		"movl %1, %%eax\n"
		"movl (%%eax), %%eax\n" // dereference this_protocol
		"movl %%eax, %0\n" // store in demoversion
		"popl %%eax\n"
		"jmpl *%2\n"
		: "=m" (demoversion)
		: "m" (this_protocol), "m" (ReadDemoHeader_midpoint)
	);
}

INIT {
	con_cmdcb orig_listdemo_cb = con_findcmd("listdemo")->cb;
	if_cold (!orig_listdemo_cb) return FEAT_INCOMPAT;
	if_cold (!find_ReadDemoHeader(orig_listdemo_cb)) {
		errmsg_errorx("couldn't find ReadDemoHeader function");
		return FEAT_INCOMPAT;
	}
	if_cold (!find_midpoint()) {
		errmsg_errorx("couldn't find mid-point for ReadDemoHeader hook");
		return FEAT_INCOMPAT;
	}
	if_cold (!find_GetHostVersion()) {
		errmsg_errorx("couldn't find GetHostVersion function");
		return FEAT_INCOMPAT;
	}
	gameversion = orig_GetHostVersion();
	orig_GetHostVersion = (GetHostVersion_func)hook_inline(
			(void *)orig_GetHostVersion, (void *)&hook_GetHostVersion);
	if (!orig_GetHostVersion) {
		errmsg_errorsys("couldn't hook GetHostVersion");
		return FEAT_FAIL;
	}
	orig_ReadDemoHeader = (ReadDemoHeader_func)hook_inline(
			(void *)orig_ReadDemoHeader, (void *)&hook_ReadDemoHeader);
	if (!orig_ReadDemoHeader) {
		errmsg_errorsys("couldn't hook ReadDemoHeader");
		goto e1;
	}
	ReadDemoHeader_midpoint = hook_inline(
			(void *)ReadDemoHeader_midpoint, (void *)&hook_midpoint);
	if (!ReadDemoHeader_midpoint) {
		errmsg_errorsys("couldn't hook ReadDemoHeader midpoint");
		goto e2;
	}
	return FEAT_OK;

e2:	unhook_inline((void *)orig_ReadDemoHeader);
e1:	unhook_inline((void *)orig_GetHostVersion);
	return FEAT_FAIL;
}

END {
	if_cold (sst_userunloaded) {
		unhook_inline((void *)ReadDemoHeader_midpoint);
		unhook_inline((void *)orig_ReadDemoHeader);
		unhook_inline((void *)orig_GetHostVersion);
	}
}

// vi: sw=4 ts=4 noet tw=80 cc=80