summaryrefslogtreecommitdiffhomepage
path: root/src/fov.c
blob: 8319ed19b905b841adbfe142e655bf138dc31ab2 (plain)
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
/*
 * Copyright © 2022 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.
 */

// TODO(linux): theoretically, probably ifdef out the cvar-replacement stuff; we
// expect any game that's been ported to linux to already have fov_desired

#include "con_.h"
#include "engineapi.h"
#include "errmsg.h"
#include "ent.h"
#include "event.h"
#include "feature.h"
#include "gametype.h"
#include "hook.h"
#include "intdefs.h"
#include "mem.h"
#include "sst.h"
#include "vcall.h"
#include "x86.h"
#include "x86util.h"

FEATURE("extended FOV range")
REQUEST(ent)

DEF_CVAR_MINMAX_UNREG(fov_desired,
		"Set the base field of view (SST reimplementation)", 75, 75, 120,
		CON_HIDDEN | CON_ARCHIVE)
static struct con_var *real_fov_desired; // engine's if it has it, or ours

typedef void (*VCALLCONV SetDefaultFOV_func)(void *, int);
static SetDefaultFOV_func orig_SetDefaultFOV;
static void VCALLCONV hook_SetDefaultFOV(void *this, int fov) {
	// the game normally clamps fov_desired on the server side, disregard
	// whatever it tries to set and force our own value instead
	orig_SetDefaultFOV(this, con_getvari(real_fov_desired));
}

static bool find_SetDefaultFOV(struct con_cmd *fov) {
	uchar *fovcb = (uchar *)fov->cb;
	int callcnt = 0;
	for (uchar *p = fovcb; p - fovcb < 96;) {
		// fov command source, and consequent asm, calls 4 functions, one of
		// them virtual (i.e. via register). of the 3 direct calls,
		// SetDefaultFOV is the third.
		if (p[0] == X86_CALL && ++callcnt == 3) {
			orig_SetDefaultFOV = (SetDefaultFOV_func)(p + 5 +
					mem_loadoffset(p + 1));
			return true;
		}
		NEXT_INSN(p, "SetDefaultFOV");
	}
	return false;
}

// replacement cvar needs to actively set player fov if in a map
static void fovcb(struct con_var *v) {
	void *player = ent_get(1); // NOTE: singleplayer only!
	if (player) orig_SetDefaultFOV(player, con_getvari(v));
}

// ensure FOV is applied on load, if the engine wouldn't do that itself
HANDLE_EVENT(ClientActive, struct edict *e) {
	if (real_fov_desired == fov_desired) {
		orig_SetDefaultFOV(e->ent_unknown, con_getvari(fov_desired));
	}
}

static struct con_cmd *cmd_fov;

PREINIT {
	// could work for other games, but generally only portal 1 people want this
	// (the rest of us consider this cheating and a problem for runs...)
	return GAMETYPE_MATCHES(Portal1);
}

INIT {
	cmd_fov = con_findcmd("fov");
	if (!cmd_fov) return false; // shouldn't really happen but just in case!
	if (real_fov_desired = con_findvar("fov_desired")) {
		// latest steampipe already goes up to 120 fov
		if (real_fov_desired->parent->maxval == 120) return false;
		real_fov_desired->parent->maxval = 120;
	}
	else {
		if (!has_ent) return false;
		con_reg(fov_desired);
		real_fov_desired = fov_desired;
	}
	if (!find_SetDefaultFOV(cmd_fov)) {
		errmsg_errorx("couldn't find SetDefaultFOV function");
		return false;
	}
	orig_SetDefaultFOV = (SetDefaultFOV_func)hook_inline(
			(void *)orig_SetDefaultFOV, (void *)&hook_SetDefaultFOV);
	if (!orig_SetDefaultFOV) {
		errmsg_errorsys("couldn't hook SetDefaultFOV function");
		return false;
	}

	// we might not be using our cvar but simpler to do this unconditionally
	fov_desired->cb = &fovcb;
	fov_desired->parent->base.flags &= ~CON_HIDDEN;
	// hide the original fov command since we've effectively broken it anyway :)
	cmd_fov->base.flags |= CON_DEVONLY;
	return true;
}

END {
	if (real_fov_desired && real_fov_desired != fov_desired) {
		real_fov_desired->parent->maxval = 90;
		if (con_getvarf(real_fov_desired) > 90) {
			con_setvarf(real_fov_desired, 90); // blegh.
		}
	}
	else {
		void *player = ent_get(1); // also singleplayer only
		if (player) orig_SetDefaultFOV(player, 75);
	}
	unhook_inline((void *)orig_SetDefaultFOV);
	cmd_fov->base.flags &= ~CON_DEVONLY;
}

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