summaryrefslogtreecommitdiff
path: root/src/l4d1democompat.c
blob: 735215c2800bf81dcff3889782c961d516257eea (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
187
188
189
190
191
192
193
194
195
196
197
198
/*
 * Copyright © Hayden K <imaciidz@gmail.com>
 * Copyright © Willian Henrique <wsimanbrazil@yahoo.com.br>
 * Copyright © 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 "asm.h"
#include "chunklets/x86.h"
#include "con_.h"
#include "errmsg.h"
#include "feature.h"
#include "gametype.h"
#include "hook.h"
#include "intdefs.h"
#include "mem.h"
#include "sst.h"
#include "vcall.h"
#include "x86util.h"

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

struct CDemoFile;
// 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(struct CDemoFile, 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)();
DEF_INLINE_HOOK_NOTRAMPOLINE(GetHostVersion_func, GetHostVersion)

typedef void (*VCALLCONV ReadDemoHeader_func)(struct CDemoFile *);
DEF_INLINE_HOOK(ReadDemoHeader_func, ReadDemoHeader)

int _l4d1democompat_demover; // extern for asm
static int gameversion;

static int hookdest_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 (_l4d1democompat_demover >= 1022 &&
			_l4d1democompat_demover <= gameversion) {
		return _l4d1democompat_demover;
	}
	return gameversion;
}

int *_l4d1democompat_thisproto; // extern for asm
static void VCALLCONV hookdest_ReadDemoHeader(struct CDemoFile *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.
	_l4d1democompat_thisproto = getptr_CDemoFile_protocol(this);
	orig_ReadDemoHeader(this);
}

// should this be in its own .S file? meh, leaving it here for now.
__asm (
	".pushsection " ASM_RWX_SECTION_STR ", \"" ASM_RWX_SECTION_FLAGS "\"\n"
	".globl " ASM_MANGLE_STR("_l4d1democompat_midpoint_hook") "\n"
	ASM_MANGLE_STR("_l4d1democompat_midpoint_hook") ":\n"
	// like in con_.S, we have to assemble stuff manually here, annoyingly.
	// note: ebx is safe to clobber here because we're hooking immediately after
	// a call, ebx is caller-save, and also ebx is explicitly pushed before the
	// call which means we know the code we're hooking obeys the calling conv.
	// see also find_midpoint() below.
	// mov ebx, [_l4d1democompat_thisproto]
	".byte 0x8B\n"
	".byte 0x1D\n"
	".long " ASM_MANGLE_STR("_l4d1democompat_thisproto") "\n"
	// mov ebx, [ebx]
	".byte 0x8B\n"
	".byte 0x1B\n"
	// mov [_l4d1democompat_demover], ebx
	".byte 0x89\n"
	".byte 0x1D\n"
	".long " ASM_MANGLE_STR("_l4d1democompat_demover") "\n"
	// and then we leave trampoline space immediately after; we can jump
	// straight back from here to the rest of the function!
	".globl " ASM_MANGLE_STR("_l4d1democompat_midpoint_trampoline") "\n"
	ASM_MANGLE_STR("_l4d1democompat_midpoint_trampoline") ":\n"
	".space 24\n"
	".popsection\n"
);
// XXX: dummy function prototype. not to be called directly, just to pass into
// the inline hooking machinery.
// maybe one day we'll come up with a marginally cleaner way of doing these
// midpoint hooks, although there's not many uses currently so no big deal.
int _l4d1democompat_midpoint_hook();
int _l4d1democompat_midpoint_trampoline();
DEF_INLINE_HOOK_WITHTRAMPOLINE(int (*)(), ReadDemoHeader_midpoint,
		_l4d1democompat_midpoint_trampoline)

static inline ReadDemoHeader_func find_ReadDemoHeader(const uchar *insns) {
	// Find the call to ReadDemoHeader in the listdemo callback
	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) {
			return (ReadDemoHeader_func)(p + 12 + mem_loads32(p + 8));
		}
		NEXT_INSN(p, "ReadDemoHeader");
	}
	return 0;
}

static inline void *find_midpoint(ReadDemoHeader_func ReadDemoHeader) {
	uchar *insns = (uchar *)ReadDemoHeader;
	for (uchar *p = insns; p - insns < 128;) {
		const u64 HL2DEMO = 0x4F4D4544324C48; // "HL2DEMO\0" ascii, little-endian
		if (p[0] == X86_PUSHIW && p[5] == X86_PUSHEBX && p[6] == X86_CALL &&
				!mem_loadu64(mem_loadptr(p + 1)) == HL2DEMO) {
			return p + 11;
		}
		NEXT_INSN(p, "ReadDemoHeader hook midpoint");
	}
	return 0;
}

static inline GetHostVersion_func find_GetHostVersion(
		ReadDemoHeader_func ReadDemoHeader) {
	uchar *insns = (uchar *)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) {
			return (GetHostVersion_func)(p + 7 + mem_loads32(p + 3));
		}
		NEXT_INSN(p, "GetHostVersion");
	}
	return 0;
}

INIT {
	struct con_cmd *cmd_listdemo = con_findcmd("listdemo");
	if_cold (!cmd_listdemo) return FEAT_INCOMPAT; // should never happen!
	ReadDemoHeader_func ReadDemoHeader = find_ReadDemoHeader(
			cmd_listdemo->cb_insns);
	if_cold (!ReadDemoHeader) {
		errmsg_errorx("couldn't find ReadDemoHeader function");
		return FEAT_INCOMPAT;
	}
	void *midpoint = find_midpoint(ReadDemoHeader);
	if_cold (!midpoint) {
		errmsg_errorx("couldn't find mid-point for ReadDemoHeader hook");
		return FEAT_INCOMPAT;
	}
	GetHostVersion_func GetHostVersion = find_GetHostVersion(ReadDemoHeader);
	if_cold (!GetHostVersion) {
		errmsg_errorx("couldn't find GetHostVersion function");
		return FEAT_INCOMPAT;
	}
	gameversion = GetHostVersion();
	int err = hook_featsetup_GetHostVersion(GetHostVersion);
	if_cold (err) return err;
	struct hook_featsetup_ret_ReadDemoHeader h2 =
			hook_featsetup_ReadDemoHeader(ReadDemoHeader);
	if_cold (h2.err) return h2.err;
	struct hook_featsetup_ret_ReadDemoHeader_midpoint h3 =
			hook_featsetup_ReadDemoHeader_midpoint((int (*)())midpoint);
	if_cold (h3.err) return h3.err;
	hook_commit_GetHostVersion(&hookdest_GetHostVersion);
	hook_commit_ReadDemoHeader(h2.hookpos, &hookdest_ReadDemoHeader);
	hook_commit_ReadDemoHeader_midpoint(h3.hookpos,
			&_l4d1democompat_midpoint_hook);
	return FEAT_OK;
}

END {
	if_cold (sst_userunloaded) {
		unhook_ReadDemoHeader_midpoint();
		unhook_ReadDemoHeader();
		unhook_GetHostVersion();
	}
}

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