samp_sdk/omp/players.rs
1//! Player events straight from the Open Multiplayer server.
2//!
3//! A plugin that wants `OnPlayerConnect` today goes through Pawn: the SDK
4//! detours `amx_Exec` and watches the gamemode's callbacks (see
5//! [`crate::omp::events`]). That works on both servers, but it only sees what
6//! the script is told, and it costs a detour.
7//!
8//! Open Multiplayer offers the same events natively: `ICore` hands out an
9//! `IPlayerPool`, the pool hands out an `IEventDispatcher<PlayerConnectEventHandler>`,
10//! and a component registers a handler on it. No Pawn, no detour — the server
11//! calls the plugin directly.
12//!
13//! ## Slots, per ABI
14//!
15//! | Method | Itanium | MSVC |
16//! | ------ | :-----: | :--: |
17//! | `ICore::getPlayers` | 8 | 7 |
18//! | `IPlayerPool::getPlayerConnectDispatcher` | 10 | 9 |
19//!
20//! Itanium values come from the vtable dumps of the official `omp-server`
21//! (`PlayerPool` and `Core` keep their symbols). MSVC shifts by one because it
22//! emits a single destructor slot where Itanium emits two — the same rule as
23//! everywhere else in this module, and `cargo xtask check-abi` re-derives
24//! it from the binaries.
25//!
26//! ## `PlayerConnectEventHandler`
27//!
28//! Declared in `player.hpp` with no virtual destructor, so four slots on both
29//! ABIs:
30//!
31//! ```text
32//! [0] onIncomingConnection(IPlayer&, StringView ip, u16 port)
33//! [1] onPlayerConnect(IPlayer&)
34//! [2] onPlayerDisconnect(IPlayer&, PeerDisconnectReason)
35//! [3] onPlayerClientInit(IPlayer&)
36//! ```
37
38use super::component::ICore;
39use super::types::{Colour, StringView, UID, Vector3};
40use super::vehicles::IVehicle;
41use super::vtable::{call_vtable, call_vtable_small_struct, opaque, slots, virtual_fns};
42
43slots! {
44 /// Slot of `ICore::getPlayers()`.
45 SLOT_GET_PLAYERS: usize = 8, 7;
46 /// Slot of `IPlayerPool::getPlayerConnectDispatcher()`.
47 SLOT_CONNECT_DISPATCHER: usize = 10, 9;
48}
49
50opaque! {
51 /// Opaque handle for the server's `IPlayerPool*`.
52 pub IPlayerPool;
53}
54
55slots! {
56 /// Slot of `IPlayer::kick()`.
57 SLOT_PLAYER_KICK: usize = 6, 5;
58 /// Slot of `IPlayer::isBot()`.
59 SLOT_PLAYER_IS_BOT: usize = 8, 7;
60 /// Slot of `IPlayer::getName()`.
61 SLOT_PLAYER_GET_NAME: usize = 27, 26;
62 /// Slot of `IPlayer::setHealth(float)`.
63 SLOT_PLAYER_SET_HEALTH: usize = 77, 76;
64 /// Slot of `IPlayer::getHealth()`.
65 SLOT_PLAYER_GET_HEALTH: usize = 78, 77;
66 /// Offset of the `IEntity` subobject inside an `IPlayer`.
67 ///
68 /// `IPlayer : public IExtensible, public IEntity`, so position, rotation and
69 /// virtual world live in a **secondary** vtable rather than the primary one:
70 /// the `this` pointer has to be adjusted before indexing. The offset is the
71 /// size of `IExtensible`, which is why it matches the one `OmpComponent` uses
72 /// for `IUIDProvider`. Derived from clang's record layout for both ABIs.
73 pub(crate) ENTITY_OFFSET: isize = 40, 56;
74 /// Offset of the `IReadOnlyPool<IPlayer>` subobject inside an `IPlayerPool`.
75 ///
76 /// Same reasoning as [`ENTITY_OFFSET`]: the pool interface is a secondary base,
77 /// so looking a player up by id means adjusting `this` first. From clang's
78 /// record layout of `IPlayerPool`.
79 PLAYER_POOL_OFFSET: isize = 40, 56;
80}
81
82/// `IReadOnlyPool<T>::get(int)` is its first method, and `bounds()` the second.
83/// Neither interface declares a destructor, so both ABIs agree.
84pub(crate) const SLOT_POOL_GET: usize = 0;
85pub(crate) const SLOT_POOL_BOUNDS: usize = 1;
86
87/// `IExtensible::getExtension(UID)` — slot [0] of every entity's primary
88/// vtable, since `IExtensible` is the first base and declares it first.
89const SLOT_GET_EXTENSION: usize = 0;
90
91/// Slots inside the `IEntity` vtable. It declares no destructor, so the
92/// numbering is identical on both ABIs.
93pub(crate) const SLOT_ENTITY_GET_ID: usize = 0;
94pub(crate) const SLOT_ENTITY_GET_POSITION: usize = 1;
95const SLOT_ENTITY_SET_POSITION: usize = 2;
96pub(crate) const SLOT_ENTITY_GET_ROTATION: usize = 3;
97pub(crate) const SLOT_ENTITY_SET_ROTATION: usize = 4;
98const SLOT_ENTITY_GET_VIRTUAL_WORLD: usize = 5;
99const SLOT_ENTITY_SET_VIRTUAL_WORLD: usize = 6;
100
101slots! {
102 /// Slot of `IPlayer::setMoney(int)`.
103 SLOT_PLAYER_SET_MONEY: usize = 62, 61;
104 /// Slot of `IPlayer::giveMoney(int)`.
105 SLOT_PLAYER_GIVE_MONEY: usize = 63, 62;
106 /// Slot of `IPlayer::setArmour(float)`.
107 SLOT_PLAYER_SET_ARMOUR: usize = 81, 80;
108 /// Slot of `IPlayer::getArmour()`.
109 SLOT_PLAYER_GET_ARMOUR: usize = 82, 81;
110 /// Slot of `IPlayer::setTeam(int)`.
111 SLOT_PLAYER_SET_TEAM: usize = 95, 94;
112 /// Slot of `IPlayer::getTeam()`.
113 SLOT_PLAYER_GET_TEAM: usize = 96, 95;
114 /// Slot of `IPlayer::setSkin(int, bool)`.
115 SLOT_PLAYER_SET_SKIN: usize = 97, 96;
116 /// Slots of the plain `int` accessors, all reached through
117 /// [`player_set_i32`] / [`player_get_i32`]. Every pair is one apart between
118 /// ABIs, the usual destructor shift, and comes from `cargo xtask vtable`.
119 SLOT_PLAYER_SET_DRUNK: usize = 40, 39;
120 SLOT_PLAYER_SET_WANTED: usize = 49, 48;
121 SLOT_PLAYER_GET_WANTED: usize = 50, 49;
122 SLOT_PLAYER_GET_MONEY: usize = 65, 64;
123 SLOT_PLAYER_GET_SKIN: usize = 98, 97;
124 SLOT_PLAYER_SET_WEATHER: usize = 107, 106;
125 SLOT_PLAYER_SET_INTERIOR: usize = 118, 117;
126 SLOT_PLAYER_GET_INTERIOR: usize = 119, 118;
127 /// Slot of `IPlayer::setControllable(bool)`.
128 SLOT_PLAYER_SET_CONTROLLABLE: usize = 46, 45;
129 /// Slot of `IPlayer::setScore(int)`.
130 SLOT_PLAYER_SET_SCORE: usize = 79, 78;
131 /// Slot of `IPlayer::getScore()`.
132 SLOT_PLAYER_GET_SCORE: usize = 80, 79;
133 /// Slot of `IPlayer::sendClientMessage(const Colour&, StringView)`.
134 SLOT_PLAYER_SEND_MESSAGE: usize = 100, 99;
135}
136
137opaque! {
138 /// Opaque handle for the server's `IPlayer*`, as received by a handler.
139 pub IPlayer;
140}
141
142slots! {
143 /// Slot of `IPlayerPool::getPlayerSpawnDispatcher()`.
144 SLOT_SPAWN_DISPATCHER: usize = 9, 8;
145 /// Slot of `IPlayerPool::getPlayerTextDispatcher()`.
146 SLOT_TEXT_DISPATCHER: usize = 12, 11;
147 /// Slot of `IPlayerPool::getPlayerDamageDispatcher()`.
148 SLOT_DAMAGE_DISPATCHER: usize = 15, 14;
149 /// Slot of `IPlayerPool::getPlayerStreamDispatcher()`.
150 SLOT_STREAM_DISPATCHER: usize = 11, 10;
151 /// Slot of `IPlayerPool::getPlayerShotDispatcher()`.
152 SLOT_SHOT_DISPATCHER: usize = 13, 12;
153 /// Slot of `IPlayerPool::getPlayerChangeDispatcher()`.
154 SLOT_CHANGE_DISPATCHER: usize = 14, 13;
155 /// Slot of `IPlayerPool::getPlayerClickDispatcher()`.
156 SLOT_CLICK_DISPATCHER: usize = 16, 15;
157 /// Slot of `IPlayerPool::getPlayerCheckDispatcher()`.
158 SLOT_CHECK_DISPATCHER: usize = 17, 16;
159 /// Slot of `IPlayerPool::getPlayerUpdateDispatcher()`.
160 SLOT_UPDATE_DISPATCHER: usize = 18, 17;
161}
162
163opaque! {
164 /// Opaque handle for `IEventDispatcher<PlayerConnectEventHandler>*`.
165 pub IPlayerConnectDispatcher;
166}
167
168/// Why the server dropped a player (`PeerDisconnectReason` in `network.hpp`).
169#[repr(C)]
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum DisconnectReason {
172 Timeout = 0,
173 Quit = 1,
174 Kicked = 2,
175 Custom = 3,
176 ModeEnd = 4,
177}
178
179impl DisconnectReason {
180 /// Maps the raw value the server passes, treating an unknown one as
181 /// [`DisconnectReason::Custom`] rather than transmuting it into a variant
182 /// that does not exist.
183 #[must_use]
184 pub fn from_raw(value: i32) -> Self {
185 match value {
186 0 => Self::Timeout,
187 1 => Self::Quit,
188 2 => Self::Kicked,
189 4 => Self::ModeEnd,
190 _ => Self::Custom,
191 }
192 }
193}
194
195/// `PlayerConnectEventHandler` vtable — Itanium ABI.
196#[cfg(not(target_env = "msvc"))]
197#[repr(C)]
198pub struct PlayerConnectHandlerVTable {
199 pub on_incoming_connection:
200 unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer, StringView, u16),
201 pub on_player_connect: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer),
202 pub on_player_disconnect: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer, i32),
203 pub on_player_client_init: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer),
204}
205
206/// `PlayerConnectEventHandler` vtable — MSVC ABI (`this` in ECX).
207#[cfg(target_env = "msvc")]
208#[repr(C)]
209pub struct PlayerConnectHandlerVTable {
210 pub on_incoming_connection:
211 unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer, StringView, u16),
212 pub on_player_connect: unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer),
213 pub on_player_disconnect:
214 unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer, i32),
215 pub on_player_client_init: unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer),
216}
217
218/// Object the server calls on player connection events.
219///
220/// Layout: vtable pointer at offset 0, like every C++ object with virtuals. The
221/// server keeps the pointer, so it must outlive the registration — leak it, or
222/// keep it alive for the plugin's lifetime and remove it before dropping.
223#[repr(C)]
224pub struct PlayerConnectHandler {
225 vtable: *const PlayerConnectHandlerVTable,
226}
227
228// SAFETY: the handler is only ever touched on the server's main thread.
229unsafe impl Send for PlayerConnectHandler {}
230unsafe impl Sync for PlayerConnectHandler {}
231
232impl PlayerConnectHandler {
233 /// Builds a handler backed by `vtable`.
234 #[must_use]
235 pub fn new(vtable: *const PlayerConnectHandlerVTable) -> Self {
236 Self { vtable }
237 }
238}
239
240opaque! {
241 /// Opaque handles for the objects a shot can hit, beyond the vehicle the
242 /// [`vehicles`](super::vehicles) module already defines.
243 pub IObject;
244 /// See [`IObject`].
245 pub IPlayerObject;
246 /// `PlayerBulletData`, passed by const reference — opaque here, since reading
247 /// it means pinning another layout.
248 pub PlayerBulletData;
249 /// Opaque handle for `IEventDispatcher<PlayerSpawnEventHandler>*`.
250 pub IPlayerSpawnDispatcher;
251 /// Opaque handle for `IEventDispatcher<PlayerTextEventHandler>*`.
252 pub IPlayerTextDispatcher;
253 /// Opaque handle for `IEventDispatcher<PlayerDamageEventHandler>*`.
254 pub IPlayerDamageDispatcher;
255 /// Opaque handle for `IEventDispatcher<PlayerStreamEventHandler>*`.
256 pub IPlayerStreamDispatcher;
257 /// Opaque handle for `IEventDispatcher<PlayerShotEventHandler>*`.
258 pub IPlayerShotDispatcher;
259 /// Opaque handle for `IEventDispatcher<PlayerChangeEventHandler>*`.
260 pub IPlayerChangeDispatcher;
261 /// Opaque handle for `IEventDispatcher<PlayerClickEventHandler>*`.
262 pub IPlayerClickDispatcher;
263 /// Opaque handle for `IEventDispatcher<PlayerCheckEventHandler>*`.
264 pub IPlayerCheckDispatcher;
265 /// Opaque handle for `IEventDispatcher<PlayerUpdateEventHandler>*`.
266 pub IPlayerUpdateDispatcher;
267}
268
269/// Writes a handler vtable struct for both ABIs — the server calls through it,
270/// so the convention is the platform's: `extern "C"` on Itanium, `thiscall`
271/// on MSVC. None of these handlers declares a virtual destructor, so the slot
272/// numbering is the declaration order on both.
273macro_rules! handler_vtable {
274 (
275 $(#[$meta:meta])*
276 $name:ident for $handler:ident {
277 $($field:ident: fn($($arg:ty),* $(,)?) $(-> $ret:ty)?),* $(,)?
278 }
279 ) => {
280 $(#[$meta])*
281 #[cfg(not(target_env = "msvc"))]
282 #[repr(C)]
283 pub struct $name {
284 $(pub $field: unsafe extern "C" fn(*mut $handler, $($arg),*) $(-> $ret)?),*
285 }
286
287 $(#[$meta])*
288 #[cfg(target_env = "msvc")]
289 #[repr(C)]
290 pub struct $name {
291 $(pub $field: unsafe extern "thiscall" fn(*mut $handler, $($arg),*) $(-> $ret)?),*
292 }
293
294 /// Object the server calls through [`
295 #[doc = stringify!($name)]
296 /// `]. Layout: vtable pointer at offset 0, like any C++ object with
297 /// virtuals. The server keeps the pointer, so it must outlive the
298 /// registration.
299 #[repr(C)]
300 pub struct $handler {
301 vtable: *const $name,
302 }
303
304 // SAFETY: handlers are only ever touched on the server's main thread.
305 unsafe impl Send for $handler {}
306 unsafe impl Sync for $handler {}
307
308 impl $handler {
309 /// Builds a handler backed by `vtable`.
310 #[must_use]
311 pub fn new(vtable: *const $name) -> Self {
312 Self { vtable }
313 }
314 }
315 };
316}
317
318handler_vtable! {
319 /// `PlayerSpawnEventHandler` — `onPlayerRequestSpawn` returning `false`
320 /// denies the spawn.
321 PlayerSpawnHandlerVTable for PlayerSpawnHandler {
322 on_player_request_spawn: fn(*mut IPlayer) -> bool,
323 on_player_spawn: fn(*mut IPlayer),
324 }
325}
326
327handler_vtable! {
328 /// `PlayerTextEventHandler` — `onPlayerText` returning `false` blocks the
329 /// message; `onPlayerCommandText` returning `true` marks the command as
330 /// handled.
331 PlayerTextHandlerVTable for PlayerTextHandler {
332 on_player_text: fn(*mut IPlayer, StringView) -> bool,
333 on_player_command_text: fn(*mut IPlayer, StringView) -> bool,
334 }
335}
336
337handler_vtable! {
338 /// `PlayerDamageEventHandler`. `killer` is null when nobody killed the
339 /// player, and `part` is a `BodyPart` value.
340 PlayerDamageHandlerVTable for PlayerDamageHandler {
341 on_player_death: fn(*mut IPlayer, *mut IPlayer, i32),
342 on_player_take_damage: fn(*mut IPlayer, *mut IPlayer, f32, u32, i32),
343 on_player_give_damage: fn(*mut IPlayer, *mut IPlayer, f32, u32, i32),
344 }
345}
346
347/// `ICore::getPlayers()` — the server's player pool.
348///
349/// # Safety
350/// `core` must be the `ICore*` the server passed to `on_load`.
351#[must_use]
352pub unsafe fn player_pool(core: *mut ICore) -> *mut IPlayerPool {
353 call_vtable!(
354 core.cast::<u8>(),
355 0,
356 SLOT_GET_PLAYERS,
357 () -> *mut IPlayerPool,
358 (),
359 std::ptr::null_mut()
360 )
361}
362
363/// `IPlayerPool::getPlayerConnectDispatcher()`.
364///
365/// # Safety
366/// `pool` must come from [`player_pool`].
367#[must_use]
368pub unsafe fn player_connect_dispatcher(pool: *mut IPlayerPool) -> *mut IPlayerConnectDispatcher {
369 #[cfg(not(target_env = "msvc"))]
370 type GetDispatcherFn = unsafe extern "C" fn(*mut u8) -> *mut IPlayerConnectDispatcher;
371 #[cfg(target_env = "msvc")]
372 type GetDispatcherFn = unsafe extern "thiscall" fn(*mut u8) -> *mut IPlayerConnectDispatcher;
373
374 let Some((this, f_ptr)) = (unsafe {
375 super::vtable::secondary_call_target_ptr(pool.cast::<u8>(), 0, SLOT_CONNECT_DISPATCHER)
376 }) else {
377 return std::ptr::null_mut();
378 };
379 let get_dispatcher: GetDispatcherFn = unsafe { std::mem::transmute(f_ptr) };
380 unsafe { get_dispatcher(this) }
381}
382
383/// Registers `handler` on the dispatcher (`addEventHandler`, slot [0]).
384///
385/// Returns what the server returned: `false` means the handler was already
386/// registered.
387///
388/// # Safety
389/// Both pointers must be valid, and `handler` must outlive the registration.
390pub unsafe fn add_player_connect_handler(
391 dispatcher: *mut IPlayerConnectDispatcher,
392 handler: *mut PlayerConnectHandler,
393) -> bool {
394 // `IEventDispatcher<T>` declares no destructor, so `addEventHandler` is
395 // slot [0] on both ABIs — same layout the Pawn dispatcher uses. The last
396 // argument is the priority, `EventPriority_Default`.
397 call_vtable!(
398 dispatcher.cast::<u8>(),
399 0,
400 0,
401 (*mut PlayerConnectHandler, i8) -> bool,
402 (handler, 0),
403 false
404 )
405}
406
407handler_vtable! {
408 /// `PlayerStreamEventHandler` — a player entering or leaving another's
409 /// stream radius.
410 PlayerStreamHandlerVTable for PlayerStreamHandler {
411 on_player_stream_in: fn(*mut IPlayer, *mut IPlayer),
412 on_player_stream_out: fn(*mut IPlayer, *mut IPlayer),
413 }
414}
415
416handler_vtable! {
417 /// `PlayerShotEventHandler` — returning `false` rejects the shot.
418 /// `bullet_data` points at a `PlayerBulletData` the server owns.
419 PlayerShotHandlerVTable for PlayerShotHandler {
420 on_player_shot_missed: fn(*mut IPlayer, *const PlayerBulletData) -> bool,
421 on_player_shot_player: fn(*mut IPlayer, *mut IPlayer, *const PlayerBulletData) -> bool,
422 on_player_shot_vehicle: fn(*mut IPlayer, *mut IVehicle, *const PlayerBulletData) -> bool,
423 on_player_shot_object: fn(*mut IPlayer, *mut IObject, *const PlayerBulletData) -> bool,
424 on_player_shot_player_object:
425 fn(*mut IPlayer, *mut IPlayerObject, *const PlayerBulletData) -> bool,
426 }
427}
428
429handler_vtable! {
430 /// `PlayerChangeEventHandler`. `PlayerState` and the key masks arrive as
431 /// plain integers.
432 PlayerChangeHandlerVTable for PlayerChangeHandler {
433 on_player_score_change: fn(*mut IPlayer, i32),
434 on_player_name_change: fn(*mut IPlayer, StringView),
435 on_player_interior_change: fn(*mut IPlayer, u32, u32),
436 on_player_state_change: fn(*mut IPlayer, i32, i32),
437 on_player_key_state_change: fn(*mut IPlayer, u32, u32),
438 }
439}
440
441handler_vtable! {
442 /// `PlayerClickEventHandler`. `Vector3` is passed by value, as the header
443 /// declares it.
444 PlayerClickHandlerVTable for PlayerClickHandler {
445 on_player_click_map: fn(*mut IPlayer, Vector3),
446 on_player_click_player: fn(*mut IPlayer, *mut IPlayer, i32),
447 }
448}
449
450handler_vtable! {
451 /// `PlayerCheckEventHandler` — the reply to a client check request.
452 PlayerCheckHandlerVTable for PlayerCheckHandler {
453 on_client_check_response: fn(*mut IPlayer, i32, i32, i32),
454 }
455}
456
457handler_vtable! {
458 /// `PlayerUpdateEventHandler` — fires for every player on every server
459 /// tick, so keep the body short. Returning `false` drops the update.
460 ///
461 /// `now` is a `TimePoint` (`steady_clock`, nanoseconds): one 64-bit value
462 /// passed by value, which on i686 lands on the stack either way.
463 PlayerUpdateHandlerVTable for PlayerUpdateHandler {
464 on_player_update: fn(*mut IPlayer, i64) -> bool,
465 }
466}
467
468virtual_fns! {
469 /// Calls `IPlayer::kick()` — drops the player from the server.
470 ///
471 /// # Safety
472 /// `player` must be an `IPlayer*` the server handed to a handler, and still
473 /// connected.
474 pub fn player_kick(player: IPlayer) = [0, SLOT_PLAYER_KICK];
475
476 /// `IPlayer::isBot()` — whether this "player" is an NPC.
477 ///
478 /// # Safety
479 /// See [`player_kick`].
480 #[must_use]
481 pub fn player_is_bot(player: IPlayer) -> bool = [0, SLOT_PLAYER_IS_BOT] or false;
482}
483
484/// `IPlayer::getName()` — the player's name, copied into a `String`.
485///
486/// The server returns a `StringView` into memory it owns, so the bytes are
487/// copied out rather than borrowed. `None` when the view is empty or not valid
488/// UTF-8.
489///
490/// # Safety
491/// See [`player_kick`].
492#[must_use]
493pub unsafe fn player_name(player: *mut IPlayer) -> Option<String> {
494 let view = call_vtable_small_struct!(
495 player.cast::<u8>(),
496 0,
497 SLOT_PLAYER_GET_NAME,
498 StringView,
499 StringView::EMPTY
500 )?;
501 unsafe { view.to_owned_string() }
502}
503
504virtual_fns! {
505 /// `IPlayer::getHealth()`.
506 ///
507 /// # Safety
508 /// See [`player_kick`].
509 #[must_use]
510 pub fn player_health(player: IPlayer) -> f32 = [0, SLOT_PLAYER_GET_HEALTH] or 0.0;
511
512 /// `IPlayer::setHealth(float)`.
513 ///
514 /// # Safety
515 /// See [`player_kick`].
516 pub fn player_set_health(player: IPlayer, health: f32) = [0, SLOT_PLAYER_SET_HEALTH];
517
518 /// `IPlayer::getScore()`.
519 ///
520 /// # Safety
521 /// See [`player_kick`].
522 #[must_use]
523 pub fn player_score(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_SCORE] or 0;
524
525 /// `IPlayer::setScore(int)`.
526 ///
527 /// Unlike health, the score is the server's own value — a client cannot
528 /// overwrite it on the next sync packet.
529 ///
530 /// # Safety
531 /// See [`player_kick`].
532 pub fn player_set_score(player: IPlayer, score: i32) = [0, SLOT_PLAYER_SET_SCORE];
533
534 /// `IEntity::getPosition()` — where the player is.
535 ///
536 /// A `Vector3` is twelve bytes, too large to come back in registers, so the
537 /// caller supplies a hidden pointer. The details differ per ABI and are easy to
538 /// get wrong by hand — on i386 System V the pointer goes **before** `this` and
539 /// the callee pops it (`ret $0x4` in the server's own `Player::getPosition`),
540 /// so writing the pointer as an ordinary argument unbalances the stack.
541 ///
542 /// Declaring the return type and letting the compiler apply the rule avoids all
543 /// of that.
544 ///
545 /// # Safety
546 /// See [`player_kick`].
547 #[must_use]
548 pub fn player_position(player: IPlayer) -> Vector3 = [ENTITY_OFFSET, SLOT_ENTITY_GET_POSITION] or Vector3::ZERO;
549
550 /// `IEntity::setPosition(Vector3)` — teleports the player.
551 ///
552 /// # Safety
553 /// See [`player_kick`].
554 pub fn player_set_position(player: IPlayer, position: Vector3) = [ENTITY_OFFSET, SLOT_ENTITY_SET_POSITION];
555
556 /// `IEntity::getVirtualWorld()`.
557 ///
558 /// # Safety
559 /// See [`player_kick`].
560 #[must_use]
561 pub fn player_virtual_world(player: IPlayer) -> i32 = [ENTITY_OFFSET, SLOT_ENTITY_GET_VIRTUAL_WORLD] or 0;
562
563 /// `IEntity::setVirtualWorld(int)`.
564 ///
565 /// # Safety
566 /// See [`player_kick`].
567 pub fn player_set_virtual_world(player: IPlayer, world: i32) = [ENTITY_OFFSET, SLOT_ENTITY_SET_VIRTUAL_WORLD];
568
569 /// `IPlayer::setMoney(int)`.
570 ///
571 /// # Safety
572 /// See [`player_kick`].
573 pub fn player_set_money(player: IPlayer, amount: i32) = [0, SLOT_PLAYER_SET_MONEY];
574
575 /// `IPlayer::giveMoney(int)` — adds to what the player already has.
576 ///
577 /// # Safety
578 /// See [`player_kick`].
579 pub fn player_give_money(player: IPlayer, amount: i32) = [0, SLOT_PLAYER_GIVE_MONEY];
580
581 /// `IPlayer::setTeam(int)`.
582 ///
583 /// # Safety
584 /// See [`player_kick`].
585 pub fn player_set_team(player: IPlayer, team: i32) = [0, SLOT_PLAYER_SET_TEAM];
586
587 /// `IPlayer::getTeam()`.
588 ///
589 /// # Safety
590 /// See [`player_kick`].
591 #[must_use]
592 pub fn player_team(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_TEAM] or 0;
593
594 /// `IPlayer::setArmour(float)`.
595 ///
596 /// # Safety
597 /// See [`player_kick`].
598 pub fn player_set_armour(player: IPlayer, armour: f32) = [0, SLOT_PLAYER_SET_ARMOUR];
599
600 /// `IPlayer::getArmour()`.
601 ///
602 /// # Safety
603 /// See [`player_kick`].
604 #[must_use]
605 pub fn player_armour(player: IPlayer) -> f32 = [0, SLOT_PLAYER_GET_ARMOUR] or 0.0;
606
607 /// `IPlayer::setSkin(int, bool)` — `send` asks the server to tell the other
608 /// players about the change, which is what a script normally wants.
609 ///
610 /// # Safety
611 /// See [`player_kick`].
612 pub fn player_set_skin(player: IPlayer, skin: i32, send: bool) = [0, SLOT_PLAYER_SET_SKIN];
613
614 /// `IEntity::getID()` — the id Pawn scripts know this player by.
615 ///
616 /// # Safety
617 /// See [`player_kick`].
618 #[must_use]
619 pub fn player_id(player: IPlayer) -> i32 = [ENTITY_OFFSET, SLOT_ENTITY_GET_ID] or -1;
620
621 /// `IReadOnlyPool<IPlayer>::get(int)` — the player with that id, or null.
622 ///
623 /// Answers "who is player 7?" without touching the pool's hash set, whose
624 /// layout belongs to `robin_hood` and would have to be mirrored to iterate.
625 ///
626 /// # Safety
627 /// `pool` must come from [`player_pool`].
628 #[must_use]
629 pub fn player_by_id(pool: IPlayerPool, id: i32) -> *mut IPlayer = [PLAYER_POOL_OFFSET, SLOT_POOL_GET] or std::ptr::null_mut();
630
631 /// `IPlayer::getMoney()`.
632 ///
633 /// # Safety
634 /// See [`player_kick`].
635 #[must_use]
636 pub fn player_money(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_MONEY] or 0;
637
638 /// `IPlayer::getSkin()`.
639 ///
640 /// # Safety
641 /// See [`player_kick`].
642 #[must_use]
643 pub fn player_skin(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_SKIN] or 0;
644
645 /// `IPlayer::setWantedLevel(unsigned)` — zero to six, as the game shows.
646 ///
647 /// # Safety
648 /// See [`player_kick`].
649 pub fn player_set_wanted_level(player: IPlayer, level: u32) = [0, SLOT_PLAYER_SET_WANTED];
650
651 /// `IPlayer::getWantedLevel()`.
652 ///
653 /// # Safety
654 /// See [`player_kick`].
655 #[must_use]
656 pub fn player_wanted_level(player: IPlayer) -> u32 = [0, SLOT_PLAYER_GET_WANTED] or 0;
657
658 /// `IPlayer::setInterior(unsigned)`.
659 ///
660 /// # Safety
661 /// See [`player_kick`].
662 pub fn player_set_interior(player: IPlayer, interior: u32) = [0, SLOT_PLAYER_SET_INTERIOR];
663
664 /// `IPlayer::getInterior()`.
665 ///
666 /// # Safety
667 /// See [`player_kick`].
668 #[must_use]
669 pub fn player_interior(player: IPlayer) -> u32 = [0, SLOT_PLAYER_GET_INTERIOR] or 0;
670
671 /// `IPlayer::setWeather(int)` — for this player alone.
672 ///
673 /// # Safety
674 /// See [`player_kick`].
675 pub fn player_set_weather(player: IPlayer, weather: i32) = [0, SLOT_PLAYER_SET_WEATHER];
676
677 /// `IPlayer::setDrunkLevel(int)`.
678 ///
679 /// # Safety
680 /// See [`player_kick`].
681 pub fn player_set_drunk_level(player: IPlayer, level: i32) = [0, SLOT_PLAYER_SET_DRUNK];
682
683 /// `IPlayer::setControllable(bool)` — `false` freezes the player.
684 ///
685 /// # Safety
686 /// See [`player_kick`].
687 pub fn player_set_controllable(player: IPlayer, controllable: bool) = [0, SLOT_PLAYER_SET_CONTROLLABLE];
688}
689
690/// The range of ids a pool can hand out, as `IReadOnlyPool<T>::bounds()`
691/// reports it: inclusive on both ends.
692#[repr(C)]
693#[derive(Debug, Clone, Copy, PartialEq, Eq)]
694pub struct PoolBounds {
695 pub first: usize,
696 pub last: usize,
697}
698
699/// `IReadOnlyPool<T>::bounds()` for any pool, given the offset of its
700/// subobject.
701///
702/// Walking these ids and asking for each one is how this SDK iterates a pool.
703/// The alternative, `entries()`, returns a `robin_hood` hash set whose layout
704/// would have to be mirrored, and mirroring it wrong reads the server's memory
705/// at random.
706///
707/// # Safety
708/// `pool` must point at an object carrying `IReadOnlyPool<T>` at `offset`.
709#[must_use]
710pub unsafe fn pool_bounds(pool: *mut u8, offset: isize) -> PoolBounds {
711 // `bounds()` returns a `Pair<size_t, size_t>`. Eight bytes, but the two
712 // ABIs disagree on how: GCC hands it back in registers, while MSVC sees a
713 // type with a constructor and returns it through a hidden pointer. Rust
714 // cannot infer that difference from a `#[repr(C)]` struct — it applies the
715 // C rule, registers on both — so the MSVC side is spelled out.
716 #[cfg(not(target_env = "msvc"))]
717 type BoundsFn = unsafe extern "C" fn(*mut u8) -> PoolBounds;
718 #[cfg(target_env = "msvc")]
719 type BoundsFn = unsafe extern "thiscall" fn(*mut u8, *mut PoolBounds) -> *mut PoolBounds;
720
721 // An empty range: `first > last`, so a loop over it runs zero times.
722 let empty = PoolBounds { first: 1, last: 0 };
723 let Some((this, f_ptr)) =
724 (unsafe { super::vtable::secondary_call_target_ptr(pool, offset, SLOT_POOL_BOUNDS) })
725 else {
726 return empty;
727 };
728 let bounds: BoundsFn = unsafe { std::mem::transmute(f_ptr) };
729
730 #[cfg(not(target_env = "msvc"))]
731 let result = unsafe { bounds(this) };
732
733 #[cfg(target_env = "msvc")]
734 let result = {
735 let mut out = empty;
736 unsafe { bounds(this, &raw mut out) };
737 out
738 };
739
740 result
741}
742
743/// Every player currently in the pool.
744///
745/// Named `all_players` rather than `players`, which at a call site would read
746/// as the module of that name.
747///
748/// Named rather than so it does not read as the module
749/// of the same name at a call site.
750///
751/// Walks the ids `bounds()` reports and keeps the ones the pool answers for, so
752/// a gap in the middle costs one call and nothing else.
753///
754/// # Safety
755/// `pool` must come from [`player_pool`].
756#[must_use]
757pub unsafe fn all_players(pool: *mut IPlayerPool) -> Vec<*mut IPlayer> {
758 let bounds = unsafe { pool_bounds(pool.cast::<u8>(), PLAYER_POOL_OFFSET) };
759 let mut found = Vec::new();
760 for id in bounds.first..=bounds.last {
761 let Ok(id) = i32::try_from(id) else { break };
762 let player = unsafe { player_by_id(pool, id) };
763 if !player.is_null() {
764 found.push(player);
765 }
766 }
767 found
768}
769
770/// The player's extension registered under `uid`, or null — what the C++
771/// side's `queryExtension<T>()` answers.
772///
773/// Components attach their per-player data with `addExtension`, which files it
774/// in the extension map; the virtual `getExtension` only covers extensions a
775/// class provides by overriding it, and returns null otherwise. `queryExtension`
776/// looks in the map first and falls back to the virtual, and so does this: the
777/// map through [`extension`](super::extensions::extension), then
778/// `IExtensible::getExtension(UID)`.
779///
780/// The generated accessors (`player_dialogs`, `player_objects`, ...) return the
781/// same thing already typed.
782///
783/// # Safety
784/// See [`player_kick`].
785#[must_use]
786pub unsafe fn player_extension(player: *mut IPlayer, uid: UID) -> *mut u8 {
787 let attached = unsafe { super::extensions::extension(player.cast::<u8>(), uid) };
788 if !attached.is_null() {
789 return attached;
790 }
791 call_vtable!(
792 player.cast::<u8>(),
793 0,
794 SLOT_GET_EXTENSION,
795 (UID) -> *mut u8,
796 (uid),
797 std::ptr::null_mut()
798 )
799}
800
801/// `IPlayer::sendClientMessage(const Colour&, StringView)` — a chat line for
802/// this player only.
803///
804/// `colour` is RGBA, the order [`Colour`] stores. The text is borrowed for the
805/// duration of the call: the server copies what it needs, and a `StringView`
806/// carries a length, so no NUL terminator is required.
807///
808/// # Safety
809/// See [`player_kick`].
810pub unsafe fn player_send_message(player: *mut IPlayer, colour: Colour, text: &str) {
811 let text = StringView::of(text);
812 call_vtable!(
813 player.cast::<u8>(),
814 0,
815 SLOT_PLAYER_SEND_MESSAGE,
816 (*const Colour, StringView) -> (),
817 (&raw const colour, text),
818 ()
819 )
820}
821
822/// Writes the `get<X>Dispatcher` + `add_<x>_handler` pair for one event group.
823macro_rules! dispatcher_pair {
824 ($getter:ident -> $dispatcher:ident @ $slot:ident, $adder:ident($handler:ident)) => {
825 /// The pool's dispatcher for this event group.
826 ///
827 /// # Safety
828 /// `pool` must come from [`player_pool`].
829 #[must_use]
830 pub unsafe fn $getter(pool: *mut IPlayerPool) -> *mut $dispatcher {
831 #[cfg(not(target_env = "msvc"))]
832 type GetFn = unsafe extern "C" fn(*mut u8) -> *mut $dispatcher;
833 #[cfg(target_env = "msvc")]
834 type GetFn = unsafe extern "thiscall" fn(*mut u8) -> *mut $dispatcher;
835
836 let Some((this, f_ptr)) =
837 (unsafe { super::vtable::secondary_call_target_ptr(pool.cast::<u8>(), 0, $slot) })
838 else {
839 return std::ptr::null_mut();
840 };
841 let get: GetFn = unsafe { std::mem::transmute(f_ptr) };
842 unsafe { get(this) }
843 }
844
845 /// Registers `handler` on the dispatcher (`addEventHandler`, slot [0]).
846 ///
847 /// Returns what the server returned: `false` means it was already
848 /// registered.
849 ///
850 /// # Safety
851 /// Both pointers must be valid, and `handler` must outlive the
852 /// registration.
853 pub unsafe fn $adder(dispatcher: *mut $dispatcher, handler: *mut $handler) -> bool {
854 call_vtable!(
855 dispatcher.cast::<u8>(),
856 0,
857 0,
858 (*mut $handler, i8) -> bool,
859 (handler, 0),
860 false
861 )
862 }
863 };
864}
865
866dispatcher_pair!(player_spawn_dispatcher -> IPlayerSpawnDispatcher @ SLOT_SPAWN_DISPATCHER,
867 add_player_spawn_handler(PlayerSpawnHandler));
868dispatcher_pair!(player_text_dispatcher -> IPlayerTextDispatcher @ SLOT_TEXT_DISPATCHER,
869 add_player_text_handler(PlayerTextHandler));
870dispatcher_pair!(player_damage_dispatcher -> IPlayerDamageDispatcher @ SLOT_DAMAGE_DISPATCHER,
871 add_player_damage_handler(PlayerDamageHandler));
872
873dispatcher_pair!(player_stream_dispatcher -> IPlayerStreamDispatcher @ SLOT_STREAM_DISPATCHER,
874 add_player_stream_handler(PlayerStreamHandler));
875dispatcher_pair!(player_shot_dispatcher -> IPlayerShotDispatcher @ SLOT_SHOT_DISPATCHER,
876 add_player_shot_handler(PlayerShotHandler));
877dispatcher_pair!(player_change_dispatcher -> IPlayerChangeDispatcher @ SLOT_CHANGE_DISPATCHER,
878 add_player_change_handler(PlayerChangeHandler));
879dispatcher_pair!(player_click_dispatcher -> IPlayerClickDispatcher @ SLOT_CLICK_DISPATCHER,
880 add_player_click_handler(PlayerClickHandler));
881dispatcher_pair!(player_check_dispatcher -> IPlayerCheckDispatcher @ SLOT_CHECK_DISPATCHER,
882 add_player_check_handler(PlayerCheckHandler));
883dispatcher_pair!(player_update_dispatcher -> IPlayerUpdateDispatcher @ SLOT_UPDATE_DISPATCHER,
884 add_player_update_handler(PlayerUpdateHandler));
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn slots_match_the_official_binaries() {
892 // Dumped from `omp-server`: Core [8] getPlayers, PlayerPool [10]
893 // getPlayerConnectDispatcher. MSVC drops one destructor slot.
894 #[cfg(not(target_env = "msvc"))]
895 {
896 assert_eq!(SLOT_GET_PLAYERS, 8);
897 assert_eq!(SLOT_CONNECT_DISPATCHER, 10);
898 }
899 #[cfg(target_env = "msvc")]
900 {
901 assert_eq!(SLOT_GET_PLAYERS, 7);
902 assert_eq!(SLOT_CONNECT_DISPATCHER, 9);
903 }
904 }
905
906 #[test]
907 fn the_other_dispatcher_slots_match_the_dump() {
908 // From the `PlayerPool` vtable of the official `omp-server`:
909 // [9] spawn, [10] connect, [12] text, [15] damage.
910 #[cfg(not(target_env = "msvc"))]
911 {
912 assert_eq!(SLOT_SPAWN_DISPATCHER, 9);
913 assert_eq!(SLOT_TEXT_DISPATCHER, 12);
914 assert_eq!(SLOT_DAMAGE_DISPATCHER, 15);
915 }
916 #[cfg(target_env = "msvc")]
917 {
918 assert_eq!(SLOT_SPAWN_DISPATCHER, 8);
919 assert_eq!(SLOT_TEXT_DISPATCHER, 11);
920 assert_eq!(SLOT_DAMAGE_DISPATCHER, 14);
921 }
922 }
923
924 #[test]
925 fn every_handler_vtable_has_the_slots_its_header_declares() {
926 let pointer = std::mem::size_of::<*const ()>();
927 assert_eq!(std::mem::size_of::<PlayerSpawnHandlerVTable>(), 2 * pointer);
928 assert_eq!(std::mem::size_of::<PlayerTextHandlerVTable>(), 2 * pointer);
929 assert_eq!(
930 std::mem::size_of::<PlayerDamageHandlerVTable>(),
931 3 * pointer
932 );
933 assert_eq!(std::mem::offset_of!(PlayerSpawnHandler, vtable), 0);
934 assert_eq!(std::mem::offset_of!(PlayerTextHandler, vtable), 0);
935 assert_eq!(std::mem::offset_of!(PlayerDamageHandler, vtable), 0);
936 }
937
938 #[test]
939 fn the_remaining_dispatcher_slots_match_the_dump() {
940 // PlayerPool vtable of the official `omp-server`: [11] stream,
941 // [13] shot, [14] change, [16] click, [17] check, [18] update.
942 #[cfg(not(target_env = "msvc"))]
943 let expected = [11, 13, 14, 16, 17, 18];
944 #[cfg(target_env = "msvc")]
945 let expected = [10, 12, 13, 15, 16, 17];
946 assert_eq!(
947 [
948 SLOT_STREAM_DISPATCHER,
949 SLOT_SHOT_DISPATCHER,
950 SLOT_CHANGE_DISPATCHER,
951 SLOT_CLICK_DISPATCHER,
952 SLOT_CHECK_DISPATCHER,
953 SLOT_UPDATE_DISPATCHER,
954 ],
955 expected
956 );
957 }
958
959 #[test]
960 fn the_remaining_vtables_have_the_slots_their_headers_declare() {
961 let p = std::mem::size_of::<*const ()>();
962 assert_eq!(std::mem::size_of::<PlayerStreamHandlerVTable>(), 2 * p);
963 assert_eq!(std::mem::size_of::<PlayerShotHandlerVTable>(), 5 * p);
964 assert_eq!(std::mem::size_of::<PlayerChangeHandlerVTable>(), 5 * p);
965 assert_eq!(std::mem::size_of::<PlayerClickHandlerVTable>(), 2 * p);
966 assert_eq!(std::mem::size_of::<PlayerCheckHandlerVTable>(), p);
967 assert_eq!(std::mem::size_of::<PlayerUpdateHandlerVTable>(), p);
968 }
969
970 #[test]
971 fn player_method_slots_match_the_dump() {
972 // `Player` vtable of the official `omp-server`: [6] kick, [8] isBot,
973 // [27] getName. MSVC drops the second destructor slot, and no
974 // `IEntity` override (secondary base) sits before any of these, so the
975 // shift is exactly one. Unlike the component classes, the Windows
976 // server carries no RTTI for `Player`, so these cannot be re-derived
977 // from the binary — `cargo xtask vtable` derives them from the
978 // headers with clang, and a running server proves them.
979 #[cfg(not(target_env = "msvc"))]
980 let expected = [6, 8, 27, 77, 78, 79, 80, 100];
981 #[cfg(target_env = "msvc")]
982 let expected = [5, 7, 26, 76, 77, 78, 79, 99];
983
984 // The plain accessors added later, same source, same shift.
985 #[cfg(not(target_env = "msvc"))]
986 assert_eq!(
987 [
988 SLOT_PLAYER_SET_DRUNK,
989 SLOT_PLAYER_SET_CONTROLLABLE,
990 SLOT_PLAYER_SET_WANTED,
991 SLOT_PLAYER_GET_WANTED,
992 SLOT_PLAYER_GET_MONEY,
993 SLOT_PLAYER_GET_SKIN,
994 SLOT_PLAYER_SET_WEATHER,
995 SLOT_PLAYER_SET_INTERIOR,
996 SLOT_PLAYER_GET_INTERIOR,
997 ],
998 [40, 46, 49, 50, 65, 98, 107, 118, 119]
999 );
1000 #[cfg(target_env = "msvc")]
1001 assert_eq!(
1002 [
1003 SLOT_PLAYER_SET_DRUNK,
1004 SLOT_PLAYER_SET_CONTROLLABLE,
1005 SLOT_PLAYER_SET_WANTED,
1006 SLOT_PLAYER_GET_WANTED,
1007 SLOT_PLAYER_GET_MONEY,
1008 SLOT_PLAYER_GET_SKIN,
1009 SLOT_PLAYER_SET_WEATHER,
1010 SLOT_PLAYER_SET_INTERIOR,
1011 SLOT_PLAYER_GET_INTERIOR,
1012 ],
1013 [39, 45, 48, 49, 64, 97, 106, 117, 118]
1014 );
1015 assert_eq!(
1016 [
1017 SLOT_PLAYER_KICK,
1018 SLOT_PLAYER_IS_BOT,
1019 SLOT_PLAYER_GET_NAME,
1020 SLOT_PLAYER_SET_HEALTH,
1021 SLOT_PLAYER_GET_HEALTH,
1022 SLOT_PLAYER_SET_SCORE,
1023 SLOT_PLAYER_GET_SCORE,
1024 SLOT_PLAYER_SEND_MESSAGE,
1025 ],
1026 expected
1027 );
1028 }
1029
1030 #[test]
1031 fn the_entity_subobject_sits_where_iextensible_ends() {
1032 // clang's record layout for `IPlayer`: the `IEntity` base starts at 40
1033 // under Itanium and 56 under MSVC — the size of `IExtensible` on each,
1034 // the same number `OmpComponent` uses for its `IUIDProvider`.
1035 #[cfg(not(target_env = "msvc"))]
1036 assert_eq!(ENTITY_OFFSET, 40);
1037 #[cfg(target_env = "msvc")]
1038 assert_eq!(ENTITY_OFFSET, 56);
1039
1040 // `IEntity` declares no destructor, so both ABIs number it the same.
1041 assert_eq!(
1042 [
1043 SLOT_ENTITY_GET_POSITION,
1044 SLOT_ENTITY_SET_POSITION,
1045 SLOT_ENTITY_GET_VIRTUAL_WORLD,
1046 SLOT_ENTITY_SET_VIRTUAL_WORLD,
1047 ],
1048 [1, 2, 5, 6]
1049 );
1050 }
1051
1052 #[test]
1053 fn handler_layout_is_a_bare_vtable_pointer() {
1054 assert_eq!(std::mem::offset_of!(PlayerConnectHandler, vtable), 0);
1055 assert_eq!(
1056 std::mem::size_of::<PlayerConnectHandler>(),
1057 std::mem::size_of::<*const ()>()
1058 );
1059 assert_eq!(
1060 std::mem::size_of::<PlayerConnectHandlerVTable>(),
1061 4 * std::mem::size_of::<*const ()>(),
1062 "four slots: no virtual destructor in the header"
1063 );
1064 }
1065
1066 #[test]
1067 fn unknown_disconnect_reason_does_not_invent_a_variant() {
1068 assert_eq!(DisconnectReason::from_raw(1), DisconnectReason::Quit);
1069 assert_eq!(DisconnectReason::from_raw(99), DisconnectReason::Custom);
1070 }
1071}