metamod_source 0.1.0-alpha.7

Higher-level bindings for Metamod:Source.
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! Callbacks for each server frame, each level, and each message clients
//! send, through Metamod's hooks and listeners.

#[cfg(test)]
#[path = "tests/server_hooks.rs"]
mod tests;

use crate::MetamodApi;

use crate::hook::{
	Handler, HookAction, HookCall, HookError, HookId, HookTarget, HookTiming, VirtualFunction,
};

use crate::sys::plugin::{self as raw, HookStatus};
use source_sdk_2013::interfaces::ServerGameDll;

use source_sdk_2013::net::incoming::{
	HookTargetError, IncomingHandler, IncomingKind, Verdict, hook_target, route_incoming,
};

use source_sdk_2013::raw::interfaces::server_game_dll::{
	GAME_FRAME_SLOT, GameFrameFn as GameFrame,
};

use source_sdk_2013::raw::net::incoming::ProcessMessageFn as ProcessMessage;
use source_sdk_2013::{Server, ServerBinding};
use std::cell::Cell;
use std::ffi::{CStr, c_char, c_int, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::{self, NonNull};

/// Runs once per server frame: before the game's own frame when installed with
/// [`MetamodApi::hook_game_frame`], and after it when installed with
/// [`MetamodApi::hook_game_frame_post`].
///
/// `simulating` can be false while the server is paused or empty, and during
/// startup before the engine has client slots. Do not assume client slots are
/// available just because this callback runs.
pub type GameFrameFn = fn(server: Server<'_>, simulating: bool);

/// `IServerGameDLL::GameFrame`, which runs the game's frame.
const GAME_FRAME: VirtualFunction<GameFrame> = VirtualFunction::new(GAME_FRAME_SLOT);

static GAME_FRAMES: Route<GameFrameFn> = Route::new();
static GAME_FRAMES_POST: Route<GameFrameFn> = Route::new();
static LEVELS: LevelRoute = LevelRoute(Cell::new(None));

/// The handler of each kind of message, in [`IncomingKind::ALL`]'s order.
static NET_MESSAGE_KINDS: [NetMessageKind; IncomingKind::ALL.len()] = {
	let mut kinds = [const { NetMessageKind(0) }; IncomingKind::ALL.len()];
	let mut kind = 0;

	while kind < kinds.len() {
		kinds[kind] = NetMessageKind(kind as c_int);
		kind += 1;
	}

	kinds
};

static NET_MESSAGES: Route<&'static dyn IncomingHandler, { IncomingKind::ALL.len() }> =
	Route::new();

/// Metamod's notifications about levels.
#[derive(Debug, Clone, Copy, Default)]
pub struct LevelEvents {
	/// Called after the game's own `LevelInit`, with the map's name.
	pub init: Option<fn(server: Server<'_>, map: &CStr)>,

	/// Called after the game's own `LevelShutdown`. Do not access the level's
	/// entities here; discard map-specific state instead.
	pub shutdown: Option<fn(server: Server<'_>)>,
}

/// The level callbacks and the server they run for, kept for the shell's
/// listener.
struct LevelRoute(Cell<Option<(ServerBinding, LevelEvents)>>);

impl LevelRoute {
	/// # Safety
	///
	/// `context` must be what [`LevelRoute::context`] returned.
	unsafe fn from_context(context: *mut c_void) -> Option<(ServerBinding, LevelEvents)> {
		// SAFETY: As the caller promises.
		unsafe { &*context.cast::<Self>() }.0.get()
	}

	fn context(&'static self) -> *mut c_void {
		ptr::from_ref(self).cast_mut().cast()
	}
}

// SAFETY: Only the server's main thread reaches it: the listener runs there,
// and `listen_level_events` takes a `MetamodApi`, which is confined to it.
unsafe impl Sync for LevelRoute {}

/// Why clients' messages could not be hooked.
#[derive(Debug, thiserror::Error)]
pub enum NetMessageHookError {
	#[error(transparent)]
	Target(#[from] HookTargetError),

	#[error(transparent)]
	Hook(#[from] HookError),
}

/// The handler of one kind of client message, by its index in
/// [`IncomingKind::ALL`].
struct NetMessageKind(c_int);

impl Handler<ProcessMessage> for NetMessageKind {
	fn call(&self, call: &HookCall<'_, ProcessMessage>) -> HookAction<bool> {
		// An earlier hook, such as SourceMod's, blocked it.
		if call.superseded() == Some(true) {
			return HookAction::Ignore;
		}

		let (message,) = call.args();

		let (Some(routed), Some(handler), Some(message)) = (
			NET_MESSAGES.get(),
			NonNull::new(call.this()),
			NonNull::new(message),
		) else {
			return HookAction::Ignore;
		};

		// SAFETY: The hook runs before the engine's handler method at this kind's
		// slot of the vtable `hook_target` found, on the main thread, with the
		// engine's handler and message. Panics are caught inside.
		match unsafe { route_incoming(&routed.binding, routed.target, self.0, handler, message) } {
			// A blocked message counts as processed, as if its handler returned
			// true.
			Verdict::Block => HookAction::Supersede(true),

			Verdict::Continue => HookAction::Ignore,
		}
	}
}

/// A callback and the server it runs for, kept for the hooks that run it.
struct Route<T, const HOOKS: usize = 1>(Cell<Option<Routed<T, HOOKS>>>);

impl<T: Copy, const HOOKS: usize> Route<T, HOOKS> {
	const fn new() -> Self {
		Self(Cell::new(None))
	}

	fn get(&self) -> Option<Routed<T, HOOKS>> {
		self.0.get()
	}

	/// Whether the route's hooks are installed, for this load of the plugin.
	fn installed(&self, api: MetamodApi<'_>) -> bool {
		self.get().is_some_and(|routed| {
			routed
				.hooks
				.into_iter()
				.flatten()
				.any(|hook| api.has_hook(hook))
		})
	}

	fn set(&self, hooks: [Option<HookId>; HOOKS], binding: ServerBinding, target: T) {
		self.0.set(Some(Routed {
			hooks,
			binding,
			target,
		}));
	}
}

impl Handler<GameFrame> for Route<GameFrameFn> {
	fn call(&self, call: &HookCall<'_, GameFrame>) -> HookAction<()> {
		let (simulating,) = call.args();

		if let Some(routed) = self.get() {
			with_server(routed.binding, |server| (routed.target)(server, simulating));
		}

		HookAction::Ignore
	}
}

// SAFETY: Only the server's main thread reaches a route: hooks only run their
// handlers there, and the functions setting them take a `MetamodApi`, which is
// confined to it.
unsafe impl<T, const HOOKS: usize> Sync for Route<T, HOOKS> {}

#[derive(Clone, Copy)]
struct Routed<T, const HOOKS: usize> {
	hooks: [Option<HookId>; HOOKS],
	binding: ServerBinding,
	target: T,
}

impl MetamodApi<'_> {
	/// Calls `callback` once per server frame, before the game's own frame.
	///
	/// This hooks `IServerGameDLL::GameFrame`. The hook stops calling back while
	/// the plugin is paused and when it unloads, and Metamod removes it after
	/// unloading the plugin. Install it while loading. To run after the frame
	/// instead, or as well, see [`Self::hook_game_frame_post`], which also
	/// describes when Metamod 2.0 starts running either hook late.
	pub fn hook_game_frame(
		self,
		game_dll: ServerGameDll<'_>,
		binding: ServerBinding,
		callback: GameFrameFn,
	) -> Result<(), HookError> {
		hook_game_frames(
			self,
			&GAME_FRAMES,
			HookTiming::Pre,
			game_dll,
			binding,
			callback,
		)
	}

	/// Calls `callback` once per server frame, after the game's own frame.
	///
	/// This hooks `IServerGameDLL::GameFrame` after the call, apart from
	/// [`Self::hook_game_frame`]: either can be installed without the other.
	/// With both, each frame runs that callback, then the game's frame, then
	/// this one. This one also runs after a frame the game cut short, or that
	/// another plugin's hook skipped. Entities removed during the frame may
	/// already be freed by then.
	///
	/// The hook stops calling back while the plugin is paused and when it
	/// unloads, and Metamod removes it after unloading the plugin. Install it
	/// while loading. Under Metamod 2.0, when `GameFrame` is already detoured,
	/// by another plugin or by this plugin's other `GameFrame` hook, KHook adds
	/// the new hook from a worker thread, so it may miss the next few frames.
	/// Of the two hooks, the one installed second is always added this way, so
	/// callbacks that pair up must handle frames where only one of them ran.
	pub fn hook_game_frame_post(
		self,
		game_dll: ServerGameDll<'_>,
		binding: ServerBinding,
		callback: GameFrameFn,
	) -> Result<(), HookError> {
		hook_game_frames(
			self,
			&GAME_FRAMES_POST,
			HookTiming::Post,
			game_dll,
			binding,
			callback,
		)
	}

	/// Passes every message a client sends to `handler`, before the engine
	/// processes it, and drops the ones it blocks.
	///
	/// This hooks each `Process*` method of the engine's client message
	/// handler, which [`hook_target`] finds and checks. The hooks stop calling
	/// back while the plugin is paused and when it unloads, and Metamod removes
	/// them after unloading the plugin. The engine creates its client objects
	/// as players first connect, so this fails with
	/// [`HookTargetError::NotReady`] until someone has.
	pub fn hook_net_messages(
		self,
		server: Server<'_>,
		binding: ServerBinding,
		handler: &'static dyn IncomingHandler,
	) -> Result<(), NetMessageHookError> {
		if NET_MESSAGES.installed(self) {
			return Err(HookError::AlreadyInstalled.into());
		}

		let target = hook_target(server)?;
		let mut hooks = [None; IncomingKind::ALL.len()];

		for (kind, &slot) in target.slots.iter().enumerate() {
			let hooked = usize::try_from(slot)
				.map_err(|_| HookError::InvalidArgument)
				.and_then(|slot| {
					// SAFETY: As for `hook_game_frames`. The target is a live handler
					// of the engine's, whose methods at the slots each take a message
					// and return `bool`, and its class lasts as long as the engine.
					unsafe {
						self.add_hook(
							VirtualFunction::<ProcessMessage>::new(slot),
							HookTarget::class_of(target.handler),
							HookTiming::Pre,
							&NET_MESSAGE_KINDS[kind],
						)
					}
				});

			match hooked {
				Ok(hook) => hooks[kind] = Some(hook),

				Err(error) => {
					for hook in hooks.into_iter().flatten() {
						self.remove_hook(hook);
					}

					return Err(error.into());
				}
			}
		}

		NET_MESSAGES.set(hooks, binding, handler);
		Ok(())
	}

	/// Passes Metamod's level notifications to `events`.
	///
	/// This registers an `IMetamodListener`. It stops calling back while the
	/// plugin is paused and when it unloads, and Metamod removes it after
	/// unloading the plugin.
	pub fn listen_level_events(
		self,
		binding: ServerBinding,
		events: LevelEvents,
	) -> Result<(), HookError> {
		LEVELS.0.set(Some((binding, events)));

		// SAFETY: A `MetamodApi` only exists during a callback, on the main
		// thread. The callbacks are functions of this library, which only read a
		// static.
		let status = unsafe {
			raw::cpp_metamod_listen_levels(
				self.version().plugin_api_version(),
				events.init.map(|_| level_init as raw::LevelInitCallback),
				events
					.shutdown
					.map(|_| level_shutdown as raw::LevelShutdownCallback),
				LEVELS.context(),
			)
		};

		match status {
			HookStatus::INSTALLED => Ok(()),
			HookStatus::NOT_BOUND => Err(HookError::NotBound),
			HookStatus::ALREADY_INSTALLED => Err(HookError::AlreadyInstalled),
			HookStatus::INVALID_ARGUMENT => Err(HookError::InvalidArgument),
			_ => Err(HookError::Unsupported),
		}
	}
}

/// Hooks `IServerGameDLL::GameFrame` at `timing`, and has the hook pass each
/// call to `callback` through `route`.
fn hook_game_frames(
	api: MetamodApi<'_>,
	route: &'static Route<GameFrameFn>,
	timing: HookTiming,
	game_dll: ServerGameDll<'_>,
	binding: ServerBinding,
	callback: GameFrameFn,
) -> Result<(), HookError> {
	if route.installed(api) {
		return Err(HookError::AlreadyInstalled);
	}

	let game_dll = NonNull::new(game_dll.as_ptr()).ok_or(HookError::InvalidArgument)?;

	// SAFETY: A `MetamodApi` only exists during a callback, on the main thread.
	// `game_dll` is the game's interface, which outlives the plugin, and has
	// `GameFrame` at the slot.
	let hook = unsafe { api.add_hook(GAME_FRAME, HookTarget::instance(game_dll), timing, route) }?;

	route.set([Some(hook)], binding, callback);
	Ok(())
}

/// The shell's `OnLevelInit` callback.
unsafe extern "C" fn level_init(context: *mut c_void, map: *const c_char) {
	// SAFETY: `listen_level_events` passes the route's context.
	let Some((binding, events)) = (unsafe { LevelRoute::from_context(context) }) else {
		return;
	};

	let (Some(init), false) = (events.init, map.is_null()) else {
		return;
	};

	// SAFETY: Metamod passes the engine's map name, which lasts for the call.
	let map = unsafe { CStr::from_ptr(map) };

	with_server(binding, |server| init(server, map));
}

/// The shell's `OnLevelShutdown` callback.
unsafe extern "C" fn level_shutdown(context: *mut c_void) {
	// SAFETY: `listen_level_events` passes the route's context.
	if let Some((
		binding,
		LevelEvents {
			shutdown: Some(shutdown),
			..
		},
	)) = unsafe { LevelRoute::from_context(context) }
	{
		with_server(binding, shutdown);
	}
}

/// Runs `f` with a server for the current call from the engine. A panic is
/// caught: it must not unwind into the engine, and the panic hook reports it.
fn with_server(binding: ServerBinding, f: impl FnOnce(Server<'_>)) {
	let scope = ();

	// SAFETY: Hooks and listeners call back on the server's main thread, during
	// a single call from the engine.
	let server = unsafe { binding.server(&scope) };

	catch_unwind(AssertUnwindSafe(|| f(server))).ok();
}