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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
/*
* 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 <string.h>
#include "con_.h"
#include "engineapi.h"
#include "errmsg.h"
#include "feature.h"
#include "gamedata.h"
#include "hook.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
#include "os.h"
#include "ppmagic.h"
#include "sst.h"
#include "vcall.h"
#include "x86.h"
#include "x86util.h"
FEATURE("Left 4 Dead 2 addon bugfixes")
GAMESPECIFIC(L4D2_2125plus)
REQUIRE_GAMEDATA(vtidx_ManageAddonsForActiveSession)
REQUIRE_GLOBAL(engclient)
// Keeping this here since it will be useful for Future Addon Feature Plans™
/*struct SAddOnMetaData { // literal psychopath capitalisation
char absolutePath;
char name;
int type; // (0 = content, 1 = mission, 2 = mode)
int unknown;
};*/
// count of how many addon metadata entries there are - this is the m_Size
// member of s_vecAddonMetaData (which is a CUtlVector<SAddOnMetaData>)
static int *addonvecsz;
static char last_mission[128] = {0}, last_gamemode[128] = {0};
static int last_addonvecsz = 0;
static bool last_disallowaddons = false;
DECL_VFUNC_DYN(void, ManageAddonsForActiveSession)
// Crazy full name: FileSystem_ManageAddonsForActiveSession. Hence the acronym.
// Note: the 4th parameter was first added in 2.2.0.4 (21 Oct 2020), but we
// don't have to worry about that since it's cdecl (and we don't use it
// ourselves, just pass it straight through).
typedef void (*FS_MAFAS_func)(bool, char *, char *, bool);
static FS_MAFAS_func orig_FS_MAFAS;
static void hook_FS_MAFAS(bool disallowaddons, char *mission, char *gamemode,
bool ismutation) {
// At the start of a map, particularly in 2204+ in campaigns with L4D1
// commons, there can be a ton of hitches due to the game trying to
// load uncached materials as models are drawn. This FS_MAFAS function is
// the main cause of materials becoming uncached. When hosting a server,
// there are 2 calls to this function per map load: first by the server
// module calling CVEngineServer::ManageAddonsForActiveSession() and then by
// the client module calling CEngineClient::ManageAddonsForActiveSession()
// (non-host players only call call the latter). Omitting the second call
// (for partially unknown reasons) fixes most hitches, but the work done by
// FS_MAFAS can be omitted in a few more cases too.
//
// The function tries to evaluate which addon VPKs should be loaded in the
// FS based on the current gamemode, campaign and addon restrictions, adding
// and removing VPKs from the FS interface as necessary, and invalidating
// many caches (material, model and audio) to ensure proper reloading of
// assets when necessary. Given that enabled addons and addon restrictions
// should not change mid-campaign and as such the parameters given to this
// function should change very rarely, we can avoid unnecessary cache
// invalidation by checking the parameter values along with whether addons
// are enabled. Both disconnecting from a server and using the addon menu
// call FS_MAFAS to allow every enabled VPK to be loaded, so we let those
// calls go through. This fixes the vast majority of laggy cases without
// breaking anything practice.
//
// As a bonus, doing all this also seems to speed up map loads by about 1s.
//
// TODO(opt): There's one unhandled edge case reconnecting the to the same
// server we were just on, if the server hasn't changed maps. It's unclear
// why hitches still occur in this case; further research is required.
int curaddonvecsz = *addonvecsz;
if (curaddonvecsz != last_addonvecsz) {
// addons list has changed, meaning we're in the main menu. we will have
// already been called with null mission and/or gamemode and reset the
// last_ things, so update the count then call the original function.
last_addonvecsz = curaddonvecsz;
goto e;
}
// if we have zero addons loaded, we can skip doing anything else.
if (!curaddonvecsz) return;
// we have some addons, which may or may not have changed. based on the
// above assumption that nothing will change *during* a campaign, cache
// campaign and mode names try to early-exit if neither has changed. the
// mission string can be empty if playing a gamemode not supported by the
// current map (such as survival on c8m1), so always call the original in
// that case since we can't know whether we changed campaigns or not.
if (mission && gamemode && *mission) {
int missionlen = strlen(mission + 1) + 1;
int gamemodelen = strlen(gamemode);
if (missionlen < sizeof(last_mission) &&
gamemodelen < sizeof(last_gamemode)) {
bool canskip = disallowaddons == last_disallowaddons &&
!strncmp(mission, last_mission, missionlen + 1) &&
!strncmp(gamemode, last_gamemode, gamemodelen + 1);
if_hot (canskip) {
disallowaddons = last_disallowaddons;
memcpy(last_mission, mission, missionlen + 1);
memcpy(last_gamemode, gamemode, gamemodelen + 1);
return;
}
}
}
// If we get here, we don't know for sure whether something might have
// changed, so we have to assume it did; we reset our cached values to avoid
// any false negatives in future, and then call the original function.
last_disallowaddons = false;
last_mission[0] = '\0';
last_gamemode[0] = '\0';
e: orig_FS_MAFAS(disallowaddons, mission, gamemode, ismutation);
}
static inline bool find_FS_MAFAS() {
#ifdef _WIN32
const uchar *insns = (const uchar *)VFUNC(engclient,
ManageAddonsForActiveSession);
// CEngineClient::ManageAddonsForActiveSession just calls FS_MAFAS
for (const uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_CALL) {
orig_FS_MAFAS = (FS_MAFAS_func)(p + 5 + mem_loads32(p + 1));
return true;
}
NEXT_INSN(p, "FileSystem_ManageAddonsForActiveSession function");
}
#else
#warning: TODO(linux): asm search stuff
#endif
return false;
}
static inline bool find_addonvecsz(con_cmdcb show_addon_metadata_cb) {
#ifdef _WIN32
const uchar *insns = (const uchar*)show_addon_metadata_cb;
// show_addon_metadata immediately checks if s_vecAddonMetadata.m_Size is 0,
// so we can just grab it from the CMP instruction
for (const uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_ALUMI8S && p[1] == X86_MODRM(0, 7, 5) && p[6] == 0) {
addonvecsz = mem_loadptr(p + 2);
return true;
}
NEXT_INSN(p, "addonvecsz variable");
}
#else
#warning: TODO(linux): asm search stuff
#endif
return false;
}
static void *broken_addon_check = 0;
static uchar orig_broken_addon_check_bytes[13];
enum { SMALLNOP = 9, BIGNOP = 13 };
static inline bool nop_addon_check(int noplen) {
// In versions prior to 2204 (21 Oct 2020), FS_MAFAS checks if any
// addons are enabled before doing anything else. If no addons are enabled,
// then the function just returns immediately. FS_MAFAS gets called by
// update_addon_paths, which is run when you click 'Done' in the addons
// menu. This means that turning off all addons breaks everything until the
// game is restarted or another addon is loaded. To fix this, we just
// replace the CMP and JZ instructions with NOPs. Depending on the version
// of the code, we either have to replace 9 bytes (e.g. 2203) or 13 bytes
// (e.g. 2147). So, we have a 9-byte NOP followed by a 4-byte NOP and can
// just use the given length value.
static const uchar nops[] =
HEXBYTES(66, 0F, 1F, 84, 00, 00, 00, 00, 00, 0F, 1F, 40, 00);
// NOTE: always using 13 for orig even though noplen can be 9 or 13; not
// worth tracking that vs. just always putting 13 bytes back later.
// Also passing 13 to mprot just in case the instructions straddle a page
// boundary (unlikely, of course).
if_hot (os_mprot(broken_addon_check, 13, PAGE_EXECUTE_READWRITE)) {
memcpy(orig_broken_addon_check_bytes, broken_addon_check, 13);
memcpy(broken_addon_check, nops, noplen);
return true;
}
else {
errmsg_warnsys("couldn't fix broken addon check: "
"couldn't make make memory writable");
return false;
}
}
static inline void try_fix_broken_addon_check() {
uchar *insns = (uchar *)orig_FS_MAFAS;
for (uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_ALUMI8S && p[1] == X86_MODRM(0, 7, 5) &&
mem_loadptr(p + 2) == addonvecsz) {
if (nop_addon_check(p[7] == X86_2BYTE ? SMALLNOP : BIGNOP)) {
broken_addon_check = p; // conditional so END doesn't crash!
}
return;
}
int len = x86_len(p);
if_cold (len == -1) {
errmsg_warnx("couldn't find broken addon check code: "
"unknown or invalid instruction");
return;
}
p += len;
}
return;
}
INIT {
struct con_cmd *show_addon_metadata = con_findcmd("show_addon_metadata");
if_cold (!show_addon_metadata) return FEAT_INCOMPAT; // shouldn't happen!
if_cold (!find_addonvecsz(show_addon_metadata->cb)) {
errmsg_errorx("couldn't find pointer to addon list");
return FEAT_INCOMPAT;
}
if_cold (!find_FS_MAFAS()) {
errmsg_errorx("couldn't find FileSystem_ManageAddonsForActiveSession");
return FEAT_INCOMPAT;
}
try_fix_broken_addon_check();
orig_FS_MAFAS = (FS_MAFAS_func)hook_inline((void *)orig_FS_MAFAS,
(void *)&hook_FS_MAFAS);
if_cold (!orig_FS_MAFAS) {
errmsg_errorsys("couldn't hook FileSystem_ManageAddonsForActiveSession");
return FEAT_FAIL;
}
return FEAT_OK;
}
END {
// TODO(opt): can this unhook be made conditional too? bill suggested it
// before but I don't know. maybe Hayden knows - mike
unhook_inline((void *)orig_FS_MAFAS);
if_cold (sst_userunloaded) {
if (broken_addon_check) {
memcpy(broken_addon_check, orig_broken_addon_check_bytes, 13);
}
}
}
// vi: sw=4 ts=4 noet tw=80 cc=80
|