Skip to main content

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 `scripts/check-abi-slots.py` 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;
42
43/// Slot of `ICore::getPlayers()`.
44#[cfg(not(target_env = "msvc"))]
45const SLOT_GET_PLAYERS: usize = 8;
46#[cfg(target_env = "msvc")]
47const SLOT_GET_PLAYERS: usize = 7;
48
49/// Slot of `IPlayerPool::getPlayerConnectDispatcher()`.
50#[cfg(not(target_env = "msvc"))]
51const SLOT_CONNECT_DISPATCHER: usize = 10;
52#[cfg(target_env = "msvc")]
53const SLOT_CONNECT_DISPATCHER: usize = 9;
54
55/// Opaque handle for the server's `IPlayerPool*`.
56#[repr(C)]
57pub struct IPlayerPool {
58    _opaque: [u8; 0],
59}
60
61/// Slot of `IPlayer::kick()`.
62#[cfg(not(target_env = "msvc"))]
63const SLOT_PLAYER_KICK: usize = 6;
64#[cfg(target_env = "msvc")]
65const SLOT_PLAYER_KICK: usize = 5;
66
67/// Slot of `IPlayer::isBot()`.
68#[cfg(not(target_env = "msvc"))]
69const SLOT_PLAYER_IS_BOT: usize = 8;
70#[cfg(target_env = "msvc")]
71const SLOT_PLAYER_IS_BOT: usize = 7;
72
73/// Slot of `IPlayer::getName()`.
74#[cfg(not(target_env = "msvc"))]
75const SLOT_PLAYER_GET_NAME: usize = 27;
76#[cfg(target_env = "msvc")]
77const SLOT_PLAYER_GET_NAME: usize = 26;
78
79/// Slot of `IPlayer::setHealth(float)`.
80#[cfg(not(target_env = "msvc"))]
81const SLOT_PLAYER_SET_HEALTH: usize = 77;
82#[cfg(target_env = "msvc")]
83const SLOT_PLAYER_SET_HEALTH: usize = 76;
84
85/// Slot of `IPlayer::getHealth()`.
86#[cfg(not(target_env = "msvc"))]
87const SLOT_PLAYER_GET_HEALTH: usize = 78;
88#[cfg(target_env = "msvc")]
89const SLOT_PLAYER_GET_HEALTH: usize = 77;
90
91/// Offset of the `IEntity` subobject inside an `IPlayer`.
92///
93/// `IPlayer : public IExtensible, public IEntity`, so position, rotation and
94/// virtual world live in a **secondary** vtable rather than the primary one:
95/// the `this` pointer has to be adjusted before indexing. The offset is the
96/// size of `IExtensible`, which is why it matches the one `OmpComponent` uses
97/// for `IUIDProvider`. Derived from clang's record layout for both ABIs.
98#[cfg(not(target_env = "msvc"))]
99pub(crate) const ENTITY_OFFSET: isize = 40;
100#[cfg(target_env = "msvc")]
101pub(crate) const ENTITY_OFFSET: isize = 56;
102
103/// Offset of the `IReadOnlyPool<IPlayer>` subobject inside an `IPlayerPool`.
104///
105/// Same reasoning as [`ENTITY_OFFSET`]: the pool interface is a secondary base,
106/// so looking a player up by id means adjusting `this` first. From clang's
107/// record layout of `IPlayerPool`.
108#[cfg(not(target_env = "msvc"))]
109const PLAYER_POOL_OFFSET: isize = 40;
110#[cfg(target_env = "msvc")]
111const PLAYER_POOL_OFFSET: isize = 56;
112
113/// `IReadOnlyPool<T>::get(int)` is its first method, and `bounds()` the second.
114/// Neither interface declares a destructor, so both ABIs agree.
115pub(crate) const SLOT_POOL_GET_PUB: usize = 0;
116const SLOT_POOL_GET: usize = SLOT_POOL_GET_PUB;
117pub(crate) const SLOT_POOL_BOUNDS: usize = 1;
118
119/// `IExtensible::getExtension(UID)` — slot [0] of every entity's primary
120/// vtable, since `IExtensible` is the first base and declares it first.
121const SLOT_GET_EXTENSION: usize = 0;
122
123/// Slots inside the `IEntity` vtable. It declares no destructor, so the
124/// numbering is identical on both ABIs.
125pub(crate) const SLOT_ENTITY_GET_ID: usize = 0;
126pub(crate) const SLOT_ENTITY_GET_POSITION: usize = 1;
127const SLOT_ENTITY_SET_POSITION: usize = 2;
128const SLOT_ENTITY_GET_VIRTUAL_WORLD: usize = 5;
129const SLOT_ENTITY_SET_VIRTUAL_WORLD: usize = 6;
130
131/// Slot of `IPlayer::setMoney(int)`.
132#[cfg(not(target_env = "msvc"))]
133const SLOT_PLAYER_SET_MONEY: usize = 62;
134#[cfg(target_env = "msvc")]
135const SLOT_PLAYER_SET_MONEY: usize = 61;
136
137/// Slot of `IPlayer::giveMoney(int)`.
138#[cfg(not(target_env = "msvc"))]
139const SLOT_PLAYER_GIVE_MONEY: usize = 63;
140#[cfg(target_env = "msvc")]
141const SLOT_PLAYER_GIVE_MONEY: usize = 62;
142
143/// Slot of `IPlayer::setArmour(float)`.
144#[cfg(not(target_env = "msvc"))]
145const SLOT_PLAYER_SET_ARMOUR: usize = 81;
146#[cfg(target_env = "msvc")]
147const SLOT_PLAYER_SET_ARMOUR: usize = 80;
148
149/// Slot of `IPlayer::getArmour()`.
150#[cfg(not(target_env = "msvc"))]
151const SLOT_PLAYER_GET_ARMOUR: usize = 82;
152#[cfg(target_env = "msvc")]
153const SLOT_PLAYER_GET_ARMOUR: usize = 81;
154
155/// Slot of `IPlayer::setTeam(int)`.
156#[cfg(not(target_env = "msvc"))]
157const SLOT_PLAYER_SET_TEAM: usize = 95;
158#[cfg(target_env = "msvc")]
159const SLOT_PLAYER_SET_TEAM: usize = 94;
160
161/// Slot of `IPlayer::getTeam()`.
162#[cfg(not(target_env = "msvc"))]
163const SLOT_PLAYER_GET_TEAM: usize = 96;
164#[cfg(target_env = "msvc")]
165const SLOT_PLAYER_GET_TEAM: usize = 95;
166
167/// Slot of `IPlayer::setSkin(int, bool)`.
168#[cfg(not(target_env = "msvc"))]
169const SLOT_PLAYER_SET_SKIN: usize = 97;
170#[cfg(target_env = "msvc")]
171const SLOT_PLAYER_SET_SKIN: usize = 96;
172
173/// Slots of the plain `int` accessors, all reached through
174/// [`player_set_i32`] / [`player_get_i32`]. Every pair is one apart between
175/// ABIs, the usual destructor shift, and comes from `scripts/omp-vtable.py`.
176#[cfg(not(target_env = "msvc"))]
177const SLOT_PLAYER_SET_DRUNK: usize = 40;
178#[cfg(target_env = "msvc")]
179const SLOT_PLAYER_SET_DRUNK: usize = 39;
180
181#[cfg(not(target_env = "msvc"))]
182const SLOT_PLAYER_SET_WANTED: usize = 49;
183#[cfg(target_env = "msvc")]
184const SLOT_PLAYER_SET_WANTED: usize = 48;
185
186#[cfg(not(target_env = "msvc"))]
187const SLOT_PLAYER_GET_WANTED: usize = 50;
188#[cfg(target_env = "msvc")]
189const SLOT_PLAYER_GET_WANTED: usize = 49;
190
191#[cfg(not(target_env = "msvc"))]
192const SLOT_PLAYER_GET_MONEY: usize = 65;
193#[cfg(target_env = "msvc")]
194const SLOT_PLAYER_GET_MONEY: usize = 64;
195
196#[cfg(not(target_env = "msvc"))]
197const SLOT_PLAYER_GET_SKIN: usize = 98;
198#[cfg(target_env = "msvc")]
199const SLOT_PLAYER_GET_SKIN: usize = 97;
200
201#[cfg(not(target_env = "msvc"))]
202const SLOT_PLAYER_SET_WEATHER: usize = 107;
203#[cfg(target_env = "msvc")]
204const SLOT_PLAYER_SET_WEATHER: usize = 106;
205
206#[cfg(not(target_env = "msvc"))]
207const SLOT_PLAYER_SET_INTERIOR: usize = 118;
208#[cfg(target_env = "msvc")]
209const SLOT_PLAYER_SET_INTERIOR: usize = 117;
210
211#[cfg(not(target_env = "msvc"))]
212const SLOT_PLAYER_GET_INTERIOR: usize = 119;
213#[cfg(target_env = "msvc")]
214const SLOT_PLAYER_GET_INTERIOR: usize = 118;
215
216/// Slot of `IPlayer::setControllable(bool)`.
217#[cfg(not(target_env = "msvc"))]
218const SLOT_PLAYER_SET_CONTROLLABLE: usize = 46;
219#[cfg(target_env = "msvc")]
220const SLOT_PLAYER_SET_CONTROLLABLE: usize = 45;
221
222/// Slot of `IPlayer::setScore(int)`.
223#[cfg(not(target_env = "msvc"))]
224const SLOT_PLAYER_SET_SCORE: usize = 79;
225#[cfg(target_env = "msvc")]
226const SLOT_PLAYER_SET_SCORE: usize = 78;
227
228/// Slot of `IPlayer::getScore()`.
229#[cfg(not(target_env = "msvc"))]
230const SLOT_PLAYER_GET_SCORE: usize = 80;
231#[cfg(target_env = "msvc")]
232const SLOT_PLAYER_GET_SCORE: usize = 79;
233
234/// Slot of `IPlayer::sendClientMessage(const Colour&, StringView)`.
235#[cfg(not(target_env = "msvc"))]
236const SLOT_PLAYER_SEND_MESSAGE: usize = 100;
237#[cfg(target_env = "msvc")]
238const SLOT_PLAYER_SEND_MESSAGE: usize = 99;
239
240/// Opaque handle for the server's `IPlayer*`, as received by a handler.
241#[repr(C)]
242pub struct IPlayer {
243    _opaque: [u8; 0],
244}
245
246/// Slot of `IPlayerPool::getPlayerSpawnDispatcher()`.
247#[cfg(not(target_env = "msvc"))]
248const SLOT_SPAWN_DISPATCHER: usize = 9;
249#[cfg(target_env = "msvc")]
250const SLOT_SPAWN_DISPATCHER: usize = 8;
251
252/// Slot of `IPlayerPool::getPlayerTextDispatcher()`.
253#[cfg(not(target_env = "msvc"))]
254const SLOT_TEXT_DISPATCHER: usize = 12;
255#[cfg(target_env = "msvc")]
256const SLOT_TEXT_DISPATCHER: usize = 11;
257
258/// Slot of `IPlayerPool::getPlayerDamageDispatcher()`.
259#[cfg(not(target_env = "msvc"))]
260const SLOT_DAMAGE_DISPATCHER: usize = 15;
261#[cfg(target_env = "msvc")]
262const SLOT_DAMAGE_DISPATCHER: usize = 14;
263
264/// Slot of `IPlayerPool::getPlayerStreamDispatcher()`.
265#[cfg(not(target_env = "msvc"))]
266const SLOT_STREAM_DISPATCHER: usize = 11;
267#[cfg(target_env = "msvc")]
268const SLOT_STREAM_DISPATCHER: usize = 10;
269
270/// Slot of `IPlayerPool::getPlayerShotDispatcher()`.
271#[cfg(not(target_env = "msvc"))]
272const SLOT_SHOT_DISPATCHER: usize = 13;
273#[cfg(target_env = "msvc")]
274const SLOT_SHOT_DISPATCHER: usize = 12;
275
276/// Slot of `IPlayerPool::getPlayerChangeDispatcher()`.
277#[cfg(not(target_env = "msvc"))]
278const SLOT_CHANGE_DISPATCHER: usize = 14;
279#[cfg(target_env = "msvc")]
280const SLOT_CHANGE_DISPATCHER: usize = 13;
281
282/// Slot of `IPlayerPool::getPlayerClickDispatcher()`.
283#[cfg(not(target_env = "msvc"))]
284const SLOT_CLICK_DISPATCHER: usize = 16;
285#[cfg(target_env = "msvc")]
286const SLOT_CLICK_DISPATCHER: usize = 15;
287
288/// Slot of `IPlayerPool::getPlayerCheckDispatcher()`.
289#[cfg(not(target_env = "msvc"))]
290const SLOT_CHECK_DISPATCHER: usize = 17;
291#[cfg(target_env = "msvc")]
292const SLOT_CHECK_DISPATCHER: usize = 16;
293
294/// Slot of `IPlayerPool::getPlayerUpdateDispatcher()`.
295#[cfg(not(target_env = "msvc"))]
296const SLOT_UPDATE_DISPATCHER: usize = 18;
297#[cfg(target_env = "msvc")]
298const SLOT_UPDATE_DISPATCHER: usize = 17;
299
300/// Opaque handle for `IEventDispatcher<PlayerConnectEventHandler>*`.
301#[repr(C)]
302pub struct IPlayerConnectDispatcher {
303    _opaque: [u8; 0],
304}
305
306/// Why the server dropped a player (`PeerDisconnectReason` in `network.hpp`).
307#[repr(C)]
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum DisconnectReason {
310    Timeout = 0,
311    Quit = 1,
312    Kicked = 2,
313    Custom = 3,
314    ModeEnd = 4,
315}
316
317impl DisconnectReason {
318    /// Maps the raw value the server passes, treating an unknown one as
319    /// [`DisconnectReason::Custom`] rather than transmuting it into a variant
320    /// that does not exist.
321    #[must_use]
322    pub fn from_raw(value: i32) -> Self {
323        match value {
324            0 => Self::Timeout,
325            1 => Self::Quit,
326            2 => Self::Kicked,
327            4 => Self::ModeEnd,
328            _ => Self::Custom,
329        }
330    }
331}
332
333/// `PlayerConnectEventHandler` vtable — Itanium ABI.
334#[cfg(not(target_env = "msvc"))]
335#[repr(C)]
336pub struct PlayerConnectHandlerVTable {
337    pub on_incoming_connection:
338        unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer, StringView, u16),
339    pub on_player_connect: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer),
340    pub on_player_disconnect: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer, i32),
341    pub on_player_client_init: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer),
342}
343
344/// `PlayerConnectEventHandler` vtable — MSVC ABI (`this` in ECX).
345#[cfg(target_env = "msvc")]
346#[repr(C)]
347pub struct PlayerConnectHandlerVTable {
348    pub on_incoming_connection:
349        unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer, StringView, u16),
350    pub on_player_connect: unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer),
351    pub on_player_disconnect:
352        unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer, i32),
353    pub on_player_client_init: unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer),
354}
355
356/// Object the server calls on player connection events.
357///
358/// Layout: vtable pointer at offset 0, like every C++ object with virtuals. The
359/// server keeps the pointer, so it must outlive the registration — leak it, or
360/// keep it alive for the plugin's lifetime and remove it before dropping.
361#[repr(C)]
362pub struct PlayerConnectHandler {
363    vtable: *const PlayerConnectHandlerVTable,
364}
365
366// SAFETY: the handler is only ever touched on the server's main thread.
367unsafe impl Send for PlayerConnectHandler {}
368unsafe impl Sync for PlayerConnectHandler {}
369
370impl PlayerConnectHandler {
371    /// Builds a handler backed by `vtable`.
372    #[must_use]
373    pub fn new(vtable: *const PlayerConnectHandlerVTable) -> Self {
374        Self { vtable }
375    }
376}
377
378/// Opaque handles for the objects a shot can hit, beyond the vehicle the
379/// [`vehicles`](super::vehicles) module already defines.
380#[repr(C)]
381pub struct IObject {
382    _opaque: [u8; 0],
383}
384
385/// See [`IObject`].
386#[repr(C)]
387pub struct IPlayerObject {
388    _opaque: [u8; 0],
389}
390
391/// `PlayerBulletData`, passed by const reference — opaque here, since reading
392/// it means pinning another layout.
393#[repr(C)]
394pub struct PlayerBulletData {
395    _opaque: [u8; 0],
396}
397
398/// Opaque handle for `IEventDispatcher<PlayerSpawnEventHandler>*`.
399#[repr(C)]
400pub struct IPlayerSpawnDispatcher {
401    _opaque: [u8; 0],
402}
403
404/// Opaque handle for `IEventDispatcher<PlayerTextEventHandler>*`.
405#[repr(C)]
406pub struct IPlayerTextDispatcher {
407    _opaque: [u8; 0],
408}
409
410/// Opaque handle for `IEventDispatcher<PlayerDamageEventHandler>*`.
411#[repr(C)]
412pub struct IPlayerDamageDispatcher {
413    _opaque: [u8; 0],
414}
415
416/// Opaque handle for `IEventDispatcher<PlayerStreamEventHandler>*`.
417#[repr(C)]
418pub struct IPlayerStreamDispatcher {
419    _opaque: [u8; 0],
420}
421
422/// Opaque handle for `IEventDispatcher<PlayerShotEventHandler>*`.
423#[repr(C)]
424pub struct IPlayerShotDispatcher {
425    _opaque: [u8; 0],
426}
427
428/// Opaque handle for `IEventDispatcher<PlayerChangeEventHandler>*`.
429#[repr(C)]
430pub struct IPlayerChangeDispatcher {
431    _opaque: [u8; 0],
432}
433
434/// Opaque handle for `IEventDispatcher<PlayerClickEventHandler>*`.
435#[repr(C)]
436pub struct IPlayerClickDispatcher {
437    _opaque: [u8; 0],
438}
439
440/// Opaque handle for `IEventDispatcher<PlayerCheckEventHandler>*`.
441#[repr(C)]
442pub struct IPlayerCheckDispatcher {
443    _opaque: [u8; 0],
444}
445
446/// Opaque handle for `IEventDispatcher<PlayerUpdateEventHandler>*`.
447#[repr(C)]
448pub struct IPlayerUpdateDispatcher {
449    _opaque: [u8; 0],
450}
451
452/// Writes a handler vtable struct for both ABIs — the server calls through it,
453/// so the convention is the platform's: `extern "C"` on Itanium, `thiscall`
454/// on MSVC. None of these handlers declares a virtual destructor, so the slot
455/// numbering is the declaration order on both.
456macro_rules! handler_vtable {
457    (
458        $(#[$meta:meta])*
459        $name:ident for $handler:ident {
460            $($field:ident: fn($($arg:ty),* $(,)?) $(-> $ret:ty)?),* $(,)?
461        }
462    ) => {
463        $(#[$meta])*
464        #[cfg(not(target_env = "msvc"))]
465        #[repr(C)]
466        pub struct $name {
467            $(pub $field: unsafe extern "C" fn(*mut $handler, $($arg),*) $(-> $ret)?),*
468        }
469
470        $(#[$meta])*
471        #[cfg(target_env = "msvc")]
472        #[repr(C)]
473        pub struct $name {
474            $(pub $field: unsafe extern "thiscall" fn(*mut $handler, $($arg),*) $(-> $ret)?),*
475        }
476
477        /// Object the server calls through [`
478        #[doc = stringify!($name)]
479        /// `]. Layout: vtable pointer at offset 0, like any C++ object with
480        /// virtuals. The server keeps the pointer, so it must outlive the
481        /// registration.
482        #[repr(C)]
483        pub struct $handler {
484            vtable: *const $name,
485        }
486
487        // SAFETY: handlers are only ever touched on the server's main thread.
488        unsafe impl Send for $handler {}
489        unsafe impl Sync for $handler {}
490
491        impl $handler {
492            /// Builds a handler backed by `vtable`.
493            #[must_use]
494            pub fn new(vtable: *const $name) -> Self {
495                Self { vtable }
496            }
497        }
498    };
499}
500
501handler_vtable! {
502    /// `PlayerSpawnEventHandler` — `onPlayerRequestSpawn` returning `false`
503    /// denies the spawn.
504    PlayerSpawnHandlerVTable for PlayerSpawnHandler {
505        on_player_request_spawn: fn(*mut IPlayer) -> bool,
506        on_player_spawn: fn(*mut IPlayer),
507    }
508}
509
510handler_vtable! {
511    /// `PlayerTextEventHandler` — `onPlayerText` returning `false` blocks the
512    /// message; `onPlayerCommandText` returning `true` marks the command as
513    /// handled.
514    PlayerTextHandlerVTable for PlayerTextHandler {
515        on_player_text: fn(*mut IPlayer, StringView) -> bool,
516        on_player_command_text: fn(*mut IPlayer, StringView) -> bool,
517    }
518}
519
520handler_vtable! {
521    /// `PlayerDamageEventHandler`. `killer` is null when nobody killed the
522    /// player, and `part` is a `BodyPart` value.
523    PlayerDamageHandlerVTable for PlayerDamageHandler {
524        on_player_death: fn(*mut IPlayer, *mut IPlayer, i32),
525        on_player_take_damage: fn(*mut IPlayer, *mut IPlayer, f32, u32, i32),
526        on_player_give_damage: fn(*mut IPlayer, *mut IPlayer, f32, u32, i32),
527    }
528}
529
530/// `ICore::getPlayers()` — the server's player pool.
531///
532/// # Safety
533/// `core` must be the `ICore*` the server passed to `on_load`.
534#[must_use]
535pub unsafe fn player_pool(core: *mut ICore) -> *mut IPlayerPool {
536    call_vtable!(
537        core.cast::<u8>(),
538        0,
539        SLOT_GET_PLAYERS,
540        () -> *mut IPlayerPool,
541        (),
542        std::ptr::null_mut()
543    )
544}
545
546/// `IPlayerPool::getPlayerConnectDispatcher()`.
547///
548/// # Safety
549/// `pool` must come from [`player_pool`].
550#[must_use]
551pub unsafe fn player_connect_dispatcher(pool: *mut IPlayerPool) -> *mut IPlayerConnectDispatcher {
552    #[cfg(not(target_env = "msvc"))]
553    type GetDispatcherFn = unsafe extern "C" fn(*mut u8) -> *mut IPlayerConnectDispatcher;
554    #[cfg(target_env = "msvc")]
555    type GetDispatcherFn = unsafe extern "thiscall" fn(*mut u8) -> *mut IPlayerConnectDispatcher;
556
557    let Some((this, f_ptr)) = (unsafe {
558        super::vtable::secondary_call_target_ptr(pool.cast::<u8>(), 0, SLOT_CONNECT_DISPATCHER)
559    }) else {
560        return std::ptr::null_mut();
561    };
562    let get_dispatcher: GetDispatcherFn = unsafe { std::mem::transmute(f_ptr) };
563    unsafe { get_dispatcher(this) }
564}
565
566/// Registers `handler` on the dispatcher (`addEventHandler`, slot [0]).
567///
568/// Returns what the server returned: `false` means the handler was already
569/// registered.
570///
571/// # Safety
572/// Both pointers must be valid, and `handler` must outlive the registration.
573pub unsafe fn add_player_connect_handler(
574    dispatcher: *mut IPlayerConnectDispatcher,
575    handler: *mut PlayerConnectHandler,
576) -> bool {
577    #[cfg(not(target_env = "msvc"))]
578    type AddFn = unsafe extern "C" fn(*mut u8, *mut PlayerConnectHandler, i8) -> bool;
579    #[cfg(target_env = "msvc")]
580    type AddFn = unsafe extern "thiscall" fn(*mut u8, *mut PlayerConnectHandler, i8) -> bool;
581
582    // `IEventDispatcher<T>` declares no destructor, so `addEventHandler` is
583    // slot [0] on both ABIs — same layout the Pawn dispatcher uses.
584    let Some((this, f_ptr)) =
585        (unsafe { super::vtable::secondary_call_target_ptr(dispatcher.cast::<u8>(), 0, 0) })
586    else {
587        return false;
588    };
589    let add: AddFn = unsafe { std::mem::transmute(f_ptr) };
590    unsafe { add(this, handler, 0) }
591}
592
593handler_vtable! {
594    /// `PlayerStreamEventHandler` — a player entering or leaving another's
595    /// stream radius.
596    PlayerStreamHandlerVTable for PlayerStreamHandler {
597        on_player_stream_in: fn(*mut IPlayer, *mut IPlayer),
598        on_player_stream_out: fn(*mut IPlayer, *mut IPlayer),
599    }
600}
601
602handler_vtable! {
603    /// `PlayerShotEventHandler` — returning `false` rejects the shot.
604    /// `bullet_data` points at a `PlayerBulletData` the server owns.
605    PlayerShotHandlerVTable for PlayerShotHandler {
606        on_player_shot_missed: fn(*mut IPlayer, *const PlayerBulletData) -> bool,
607        on_player_shot_player: fn(*mut IPlayer, *mut IPlayer, *const PlayerBulletData) -> bool,
608        on_player_shot_vehicle: fn(*mut IPlayer, *mut IVehicle, *const PlayerBulletData) -> bool,
609        on_player_shot_object: fn(*mut IPlayer, *mut IObject, *const PlayerBulletData) -> bool,
610        on_player_shot_player_object:
611            fn(*mut IPlayer, *mut IPlayerObject, *const PlayerBulletData) -> bool,
612    }
613}
614
615handler_vtable! {
616    /// `PlayerChangeEventHandler`. `PlayerState` and the key masks arrive as
617    /// plain integers.
618    PlayerChangeHandlerVTable for PlayerChangeHandler {
619        on_player_score_change: fn(*mut IPlayer, i32),
620        on_player_name_change: fn(*mut IPlayer, StringView),
621        on_player_interior_change: fn(*mut IPlayer, u32, u32),
622        on_player_state_change: fn(*mut IPlayer, i32, i32),
623        on_player_key_state_change: fn(*mut IPlayer, u32, u32),
624    }
625}
626
627handler_vtable! {
628    /// `PlayerClickEventHandler`. `Vector3` is passed by value, as the header
629    /// declares it.
630    PlayerClickHandlerVTable for PlayerClickHandler {
631        on_player_click_map: fn(*mut IPlayer, Vector3),
632        on_player_click_player: fn(*mut IPlayer, *mut IPlayer, i32),
633    }
634}
635
636handler_vtable! {
637    /// `PlayerCheckEventHandler` — the reply to a client check request.
638    PlayerCheckHandlerVTable for PlayerCheckHandler {
639        on_client_check_response: fn(*mut IPlayer, i32, i32, i32),
640    }
641}
642
643handler_vtable! {
644    /// `PlayerUpdateEventHandler` — fires for every player on every server
645    /// tick, so keep the body short. Returning `false` drops the update.
646    ///
647    /// `now` is a `TimePoint` (`steady_clock`, nanoseconds): one 64-bit value
648    /// passed by value, which on i686 lands on the stack either way.
649    PlayerUpdateHandlerVTable for PlayerUpdateHandler {
650        on_player_update: fn(*mut IPlayer, i64) -> bool,
651    }
652}
653
654/// Calls `IPlayer::kick()` — drops the player from the server.
655///
656/// # Safety
657/// `player` must be an `IPlayer*` the server handed to a handler, and still
658/// connected.
659pub unsafe fn player_kick(player: *mut IPlayer) {
660    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_KICK, () -> (), (), ())
661}
662
663/// `IPlayer::isBot()` — whether this "player" is an NPC.
664///
665/// # Safety
666/// See [`player_kick`].
667#[must_use]
668pub unsafe fn player_is_bot(player: *mut IPlayer) -> bool {
669    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_IS_BOT, () -> bool, (), false)
670}
671
672/// `IPlayer::getName()` — the player's name, copied into a `String`.
673///
674/// The server returns a `StringView` into memory it owns, so the bytes are
675/// copied out rather than borrowed. `None` when the view is empty or not valid
676/// UTF-8.
677///
678/// Both ABIs return the 8-byte `StringView` through a hidden pointer, the same
679/// shape `component_name` uses.
680///
681/// # Safety
682/// See [`player_kick`].
683#[must_use]
684pub unsafe fn player_name(player: *mut IPlayer) -> Option<String> {
685    // Return convention differs: the Itanium ABI hands back this 8-byte,
686    // trivially copyable struct in EAX:EDX, while MSVC writes it through a
687    // hidden pointer the caller supplies.
688    #[cfg(not(target_env = "msvc"))]
689    type GetNameFn = unsafe extern "C" fn(*mut u8) -> StringView;
690    #[cfg(target_env = "msvc")]
691    type GetNameFn = unsafe extern "thiscall" fn(*mut u8, *mut StringView) -> *mut StringView;
692
693    let (this, f_ptr) = unsafe {
694        super::vtable::secondary_call_target_ptr(player.cast::<u8>(), 0, SLOT_PLAYER_GET_NAME)
695    }?;
696    let get_name: GetNameFn = unsafe { std::mem::transmute(f_ptr) };
697
698    #[cfg(not(target_env = "msvc"))]
699    let view = unsafe { get_name(this) };
700
701    #[cfg(target_env = "msvc")]
702    let view = {
703        let mut view = StringView {
704            data: std::ptr::null(),
705            len: 0,
706        };
707        unsafe { get_name(this, &raw mut view) };
708        view
709    };
710    if view.data.is_null() || view.len == 0 {
711        return None;
712    }
713    let bytes = unsafe { std::slice::from_raw_parts(view.data, view.len) };
714    std::str::from_utf8(bytes).ok().map(String::from)
715}
716
717/// `IPlayer::getHealth()`.
718///
719/// # Safety
720/// See [`player_kick`].
721#[must_use]
722pub unsafe fn player_health(player: *mut IPlayer) -> f32 {
723    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_GET_HEALTH, () -> f32, (), 0.0)
724}
725
726/// `IPlayer::setHealth(float)`.
727///
728/// # Safety
729/// See [`player_kick`].
730pub unsafe fn player_set_health(player: *mut IPlayer, health: f32) {
731    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_SET_HEALTH, (f32) -> (), (health), ())
732}
733
734/// `IPlayer::getScore()`.
735///
736/// # Safety
737/// See [`player_kick`].
738#[must_use]
739pub unsafe fn player_score(player: *mut IPlayer) -> i32 {
740    #[cfg(not(target_env = "msvc"))]
741    type GetScoreFn = unsafe extern "C" fn(*mut u8) -> i32;
742    #[cfg(target_env = "msvc")]
743    type GetScoreFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
744
745    let Some((this, f_ptr)) = (unsafe {
746        super::vtable::secondary_call_target_ptr(player.cast::<u8>(), 0, SLOT_PLAYER_GET_SCORE)
747    }) else {
748        return 0;
749    };
750    let get_score: GetScoreFn = unsafe { std::mem::transmute(f_ptr) };
751    unsafe { get_score(this) }
752}
753
754/// `IPlayer::setScore(int)`.
755///
756/// Unlike health, the score is the server's own value — a client cannot
757/// overwrite it on the next sync packet.
758///
759/// # Safety
760/// See [`player_kick`].
761pub unsafe fn player_set_score(player: *mut IPlayer, score: i32) {
762    #[cfg(not(target_env = "msvc"))]
763    type SetScoreFn = unsafe extern "C" fn(*mut u8, i32);
764    #[cfg(target_env = "msvc")]
765    type SetScoreFn = unsafe extern "thiscall" fn(*mut u8, i32);
766
767    let Some((this, f_ptr)) = (unsafe {
768        super::vtable::secondary_call_target_ptr(player.cast::<u8>(), 0, SLOT_PLAYER_SET_SCORE)
769    }) else {
770        return;
771    };
772    let set_score: SetScoreFn = unsafe { std::mem::transmute(f_ptr) };
773    unsafe { set_score(this, score) };
774}
775
776/// `IEntity::getPosition()` — where the player is.
777///
778/// A `Vector3` is twelve bytes, too large to come back in registers, so the
779/// caller supplies a hidden pointer. The details differ per ABI and are easy to
780/// get wrong by hand — on i386 System V the pointer goes **before** `this` and
781/// the callee pops it (`ret $0x4` in the server's own `Player::getPosition`),
782/// so writing the pointer as an ordinary argument unbalances the stack.
783///
784/// Declaring the return type and letting the compiler apply the rule avoids all
785/// of that.
786///
787/// # Safety
788/// See [`player_kick`].
789#[must_use]
790pub unsafe fn player_position(player: *mut IPlayer) -> Vector3 {
791    let zero = Vector3 {
792        x: 0.0,
793        y: 0.0,
794        z: 0.0,
795    };
796    call_vtable!(
797        player.cast::<u8>(),
798        ENTITY_OFFSET,
799        SLOT_ENTITY_GET_POSITION,
800        () -> Vector3,
801        (),
802        zero
803    )
804}
805
806/// `IEntity::setPosition(Vector3)` — teleports the player.
807///
808/// # Safety
809/// See [`player_kick`].
810pub unsafe fn player_set_position(player: *mut IPlayer, position: Vector3) {
811    call_vtable!(
812        player.cast::<u8>(),
813        ENTITY_OFFSET,
814        SLOT_ENTITY_SET_POSITION,
815        (Vector3) -> (),
816        (position),
817        ()
818    )
819}
820
821/// `IEntity::getVirtualWorld()`.
822///
823/// # Safety
824/// See [`player_kick`].
825#[must_use]
826pub unsafe fn player_virtual_world(player: *mut IPlayer) -> i32 {
827    call_vtable!(
828        player.cast::<u8>(),
829        ENTITY_OFFSET,
830        SLOT_ENTITY_GET_VIRTUAL_WORLD,
831        () -> i32,
832        (),
833        0
834    )
835}
836
837/// `IEntity::setVirtualWorld(int)`.
838///
839/// # Safety
840/// See [`player_kick`].
841pub unsafe fn player_set_virtual_world(player: *mut IPlayer, world: i32) {
842    call_vtable!(
843        player.cast::<u8>(),
844        ENTITY_OFFSET,
845        SLOT_ENTITY_SET_VIRTUAL_WORLD,
846        (i32) -> (),
847        (world),
848        ()
849    )
850}
851
852/// Calls a `void(int)` setter on the player's primary vtable.
853unsafe fn player_set_i32(player: *mut IPlayer, slot: usize, value: i32) {
854    call_vtable!(player.cast::<u8>(), 0, slot, (i32) -> (), (value), ())
855}
856
857/// Calls an `int()` getter on the player's primary vtable.
858unsafe fn player_get_i32(player: *mut IPlayer, slot: usize) -> i32 {
859    call_vtable!(player.cast::<u8>(), 0, slot, () -> i32, (), 0)
860}
861
862/// `IPlayer::setMoney(int)`.
863///
864/// # Safety
865/// See [`player_kick`].
866pub unsafe fn player_set_money(player: *mut IPlayer, amount: i32) {
867    unsafe { player_set_i32(player, SLOT_PLAYER_SET_MONEY, amount) };
868}
869
870/// `IPlayer::giveMoney(int)` — adds to what the player already has.
871///
872/// # Safety
873/// See [`player_kick`].
874pub unsafe fn player_give_money(player: *mut IPlayer, amount: i32) {
875    unsafe { player_set_i32(player, SLOT_PLAYER_GIVE_MONEY, amount) };
876}
877
878/// `IPlayer::setTeam(int)`.
879///
880/// # Safety
881/// See [`player_kick`].
882pub unsafe fn player_set_team(player: *mut IPlayer, team: i32) {
883    unsafe { player_set_i32(player, SLOT_PLAYER_SET_TEAM, team) };
884}
885
886/// `IPlayer::getTeam()`.
887///
888/// # Safety
889/// See [`player_kick`].
890#[must_use]
891pub unsafe fn player_team(player: *mut IPlayer) -> i32 {
892    unsafe { player_get_i32(player, SLOT_PLAYER_GET_TEAM) }
893}
894
895/// `IPlayer::setArmour(float)`.
896///
897/// # Safety
898/// See [`player_kick`].
899pub unsafe fn player_set_armour(player: *mut IPlayer, armour: f32) {
900    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_SET_ARMOUR, (f32) -> (), (armour), ())
901}
902
903/// `IPlayer::getArmour()`.
904///
905/// # Safety
906/// See [`player_kick`].
907#[must_use]
908pub unsafe fn player_armour(player: *mut IPlayer) -> f32 {
909    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_GET_ARMOUR, () -> f32, (), 0.0)
910}
911
912/// `IPlayer::setSkin(int, bool)` — `send` asks the server to tell the other
913/// players about the change, which is what a script normally wants.
914///
915/// # Safety
916/// See [`player_kick`].
917pub unsafe fn player_set_skin(player: *mut IPlayer, skin: i32, send: bool) {
918    call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_SET_SKIN, (i32, bool) -> (), (skin, send), ())
919}
920
921/// `IEntity::getID()` — the id Pawn scripts know this player by.
922///
923/// # Safety
924/// See [`player_kick`].
925#[must_use]
926pub unsafe fn player_id(player: *mut IPlayer) -> i32 {
927    call_vtable!(player.cast::<u8>(), ENTITY_OFFSET, SLOT_ENTITY_GET_ID, () -> i32, (), -1)
928}
929
930/// `IReadOnlyPool<IPlayer>::get(int)` — the player with that id, or null.
931///
932/// Answers "who is player 7?" without touching the pool's hash set, whose
933/// layout belongs to `robin_hood` and would have to be mirrored to iterate.
934///
935/// # Safety
936/// `pool` must come from [`player_pool`].
937#[must_use]
938pub unsafe fn player_by_id(pool: *mut IPlayerPool, id: i32) -> *mut IPlayer {
939    call_vtable!(
940        pool.cast::<u8>(),
941        PLAYER_POOL_OFFSET,
942        SLOT_POOL_GET,
943        (i32) -> *mut IPlayer,
944        (id),
945        std::ptr::null_mut()
946    )
947}
948
949/// `IPlayer::getMoney()`.
950///
951/// # Safety
952/// See [`player_kick`].
953#[must_use]
954pub unsafe fn player_money(player: *mut IPlayer) -> i32 {
955    unsafe { player_get_i32(player, SLOT_PLAYER_GET_MONEY) }
956}
957
958/// `IPlayer::getSkin()`.
959///
960/// # Safety
961/// See [`player_kick`].
962#[must_use]
963pub unsafe fn player_skin(player: *mut IPlayer) -> i32 {
964    unsafe { player_get_i32(player, SLOT_PLAYER_GET_SKIN) }
965}
966
967/// `IPlayer::setWantedLevel(unsigned)` — zero to six, as the game shows.
968///
969/// # Safety
970/// See [`player_kick`].
971pub unsafe fn player_set_wanted_level(player: *mut IPlayer, level: u32) {
972    unsafe { player_set_i32(player, SLOT_PLAYER_SET_WANTED, level as i32) };
973}
974
975/// `IPlayer::getWantedLevel()`.
976///
977/// # Safety
978/// See [`player_kick`].
979#[must_use]
980pub unsafe fn player_wanted_level(player: *mut IPlayer) -> u32 {
981    unsafe { player_get_i32(player, SLOT_PLAYER_GET_WANTED) as u32 }
982}
983
984/// `IPlayer::setInterior(unsigned)`.
985///
986/// # Safety
987/// See [`player_kick`].
988pub unsafe fn player_set_interior(player: *mut IPlayer, interior: u32) {
989    unsafe { player_set_i32(player, SLOT_PLAYER_SET_INTERIOR, interior as i32) };
990}
991
992/// `IPlayer::getInterior()`.
993///
994/// # Safety
995/// See [`player_kick`].
996#[must_use]
997pub unsafe fn player_interior(player: *mut IPlayer) -> u32 {
998    unsafe { player_get_i32(player, SLOT_PLAYER_GET_INTERIOR) as u32 }
999}
1000
1001/// `IPlayer::setWeather(int)` — for this player alone.
1002///
1003/// # Safety
1004/// See [`player_kick`].
1005pub unsafe fn player_set_weather(player: *mut IPlayer, weather: i32) {
1006    unsafe { player_set_i32(player, SLOT_PLAYER_SET_WEATHER, weather) };
1007}
1008
1009/// `IPlayer::setDrunkLevel(int)`.
1010///
1011/// # Safety
1012/// See [`player_kick`].
1013pub unsafe fn player_set_drunk_level(player: *mut IPlayer, level: i32) {
1014    unsafe { player_set_i32(player, SLOT_PLAYER_SET_DRUNK, level) };
1015}
1016
1017/// `IPlayer::setControllable(bool)` — `false` freezes the player.
1018///
1019/// # Safety
1020/// See [`player_kick`].
1021pub unsafe fn player_set_controllable(player: *mut IPlayer, controllable: bool) {
1022    call_vtable!(
1023        player.cast::<u8>(),
1024        0,
1025        SLOT_PLAYER_SET_CONTROLLABLE,
1026        (bool) -> (),
1027        (controllable),
1028        ()
1029    )
1030}
1031
1032/// The range of ids a pool can hand out, as `IReadOnlyPool<T>::bounds()`
1033/// reports it: inclusive on both ends.
1034#[repr(C)]
1035#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1036pub struct PoolBounds {
1037    pub first: usize,
1038    pub last: usize,
1039}
1040
1041/// `IReadOnlyPool<T>::bounds()` for any pool, given the offset of its
1042/// subobject.
1043///
1044/// Walking these ids and asking for each one is how this SDK iterates a pool.
1045/// The alternative, `entries()`, returns a `robin_hood` hash set whose layout
1046/// would have to be mirrored, and mirroring it wrong reads the server's memory
1047/// at random.
1048///
1049/// # Safety
1050/// `pool` must point at an object carrying `IReadOnlyPool<T>` at `offset`.
1051#[must_use]
1052pub unsafe fn pool_bounds(pool: *mut u8, offset: isize) -> PoolBounds {
1053    // `bounds()` returns a `Pair<size_t, size_t>`. Eight bytes, but the two
1054    // ABIs disagree on how: GCC hands it back in registers, while MSVC sees a
1055    // type with a constructor and returns it through a hidden pointer. Rust
1056    // cannot infer that difference from a `#[repr(C)]` struct — it applies the
1057    // C rule, registers on both — so the MSVC side is spelled out.
1058    #[cfg(not(target_env = "msvc"))]
1059    type BoundsFn = unsafe extern "C" fn(*mut u8) -> PoolBounds;
1060    #[cfg(target_env = "msvc")]
1061    type BoundsFn = unsafe extern "thiscall" fn(*mut u8, *mut PoolBounds) -> *mut PoolBounds;
1062
1063    // An empty range: `first > last`, so a loop over it runs zero times.
1064    let empty = PoolBounds { first: 1, last: 0 };
1065    let Some((this, f_ptr)) =
1066        (unsafe { super::vtable::secondary_call_target_ptr(pool, offset, SLOT_POOL_BOUNDS) })
1067    else {
1068        return empty;
1069    };
1070    let bounds: BoundsFn = unsafe { std::mem::transmute(f_ptr) };
1071
1072    #[cfg(not(target_env = "msvc"))]
1073    let result = unsafe { bounds(this) };
1074
1075    #[cfg(target_env = "msvc")]
1076    let result = {
1077        let mut out = empty;
1078        unsafe { bounds(this, &raw mut out) };
1079        out
1080    };
1081
1082    result
1083}
1084
1085/// Every player currently in the pool.
1086///
1087/// Named `all_players` rather than `players`, which at a call site would read
1088/// as the module of that name.
1089///
1090/// Named  rather than  so it does not read as the module
1091/// of the same name at a call site.
1092///
1093/// Walks the ids `bounds()` reports and keeps the ones the pool answers for, so
1094/// a gap in the middle costs one call and nothing else.
1095///
1096/// # Safety
1097/// `pool` must come from [`player_pool`].
1098#[must_use]
1099pub unsafe fn all_players(pool: *mut IPlayerPool) -> Vec<*mut IPlayer> {
1100    let bounds = unsafe { pool_bounds(pool.cast::<u8>(), PLAYER_POOL_OFFSET) };
1101    let mut found = Vec::new();
1102    for id in bounds.first..=bounds.last {
1103        let Ok(id) = i32::try_from(id) else { break };
1104        let player = unsafe { player_by_id(pool, id) };
1105        if !player.is_null() {
1106            found.push(player);
1107        }
1108    }
1109    found
1110}
1111
1112/// `IExtensible::getExtension(UID)` on a player.
1113///
1114/// **This reaches less than it looks like it should.** Components attach their
1115/// per-player data with `addExtension`, which files it in a `robin_hood` map
1116/// that the virtual `getExtension` does not consult — the C++ side finds it
1117/// through `queryExtension<T>()`, a template that checks the map first and only
1118/// then calls the virtual. So the stock components' data (dialogs, checkpoints,
1119/// a player's menu) comes back null here.
1120///
1121/// What this does reach is an extension a component exposes by overriding
1122/// `getExtension` itself. For the rest, the Pawn natives are the working route:
1123/// `Amx::call_native("ShowPlayerDialog", ...)`. Reading the map would mean
1124/// mirroring `robin_hood`'s layout, which this SDK does not do — see the note
1125/// on `entries()` in [`pool_bounds`].
1126///
1127/// # Safety
1128/// See [`player_kick`].
1129#[must_use]
1130pub unsafe fn player_extension(player: *mut IPlayer, uid: UID) -> *mut u8 {
1131    call_vtable!(
1132        player.cast::<u8>(),
1133        0,
1134        SLOT_GET_EXTENSION,
1135        (UID) -> *mut u8,
1136        (uid),
1137        std::ptr::null_mut()
1138    )
1139}
1140
1141/// `IPlayer::sendClientMessage(const Colour&, StringView)` — a chat line for
1142/// this player only.
1143///
1144/// `colour` is RGBA, the order [`Colour`] stores. The text is borrowed for the
1145/// duration of the call: the server copies what it needs, and a `StringView`
1146/// carries a length, so no NUL terminator is required.
1147///
1148/// # Safety
1149/// See [`player_kick`].
1150pub unsafe fn player_send_message(player: *mut IPlayer, colour: Colour, text: &str) {
1151    let text = StringView {
1152        data: text.as_ptr(),
1153        len: text.len(),
1154    };
1155    call_vtable!(
1156        player.cast::<u8>(),
1157        0,
1158        SLOT_PLAYER_SEND_MESSAGE,
1159        (*const Colour, StringView) -> (),
1160        (&raw const colour, text),
1161        ()
1162    )
1163}
1164
1165/// Writes the `get<X>Dispatcher` + `add_<x>_handler` pair for one event group.
1166macro_rules! dispatcher_pair {
1167    ($getter:ident -> $dispatcher:ident @ $slot:ident, $adder:ident($handler:ident)) => {
1168        /// The pool's dispatcher for this event group.
1169        ///
1170        /// # Safety
1171        /// `pool` must come from [`player_pool`].
1172        #[must_use]
1173        pub unsafe fn $getter(pool: *mut IPlayerPool) -> *mut $dispatcher {
1174            #[cfg(not(target_env = "msvc"))]
1175            type GetFn = unsafe extern "C" fn(*mut u8) -> *mut $dispatcher;
1176            #[cfg(target_env = "msvc")]
1177            type GetFn = unsafe extern "thiscall" fn(*mut u8) -> *mut $dispatcher;
1178
1179            let Some((this, f_ptr)) =
1180                (unsafe { super::vtable::secondary_call_target_ptr(pool.cast::<u8>(), 0, $slot) })
1181            else {
1182                return std::ptr::null_mut();
1183            };
1184            let get: GetFn = unsafe { std::mem::transmute(f_ptr) };
1185            unsafe { get(this) }
1186        }
1187
1188        /// Registers `handler` on the dispatcher (`addEventHandler`, slot [0]).
1189        ///
1190        /// Returns what the server returned: `false` means it was already
1191        /// registered.
1192        ///
1193        /// # Safety
1194        /// Both pointers must be valid, and `handler` must outlive the
1195        /// registration.
1196        pub unsafe fn $adder(dispatcher: *mut $dispatcher, handler: *mut $handler) -> bool {
1197            #[cfg(not(target_env = "msvc"))]
1198            type AddFn = unsafe extern "C" fn(*mut u8, *mut $handler, i8) -> bool;
1199            #[cfg(target_env = "msvc")]
1200            type AddFn = unsafe extern "thiscall" fn(*mut u8, *mut $handler, i8) -> bool;
1201
1202            let Some((this, f_ptr)) = (unsafe {
1203                super::vtable::secondary_call_target_ptr(dispatcher.cast::<u8>(), 0, 0)
1204            }) else {
1205                return false;
1206            };
1207            let add: AddFn = unsafe { std::mem::transmute(f_ptr) };
1208            unsafe { add(this, handler, 0) }
1209        }
1210    };
1211}
1212
1213dispatcher_pair!(player_spawn_dispatcher -> IPlayerSpawnDispatcher @ SLOT_SPAWN_DISPATCHER,
1214                 add_player_spawn_handler(PlayerSpawnHandler));
1215dispatcher_pair!(player_text_dispatcher -> IPlayerTextDispatcher @ SLOT_TEXT_DISPATCHER,
1216                 add_player_text_handler(PlayerTextHandler));
1217dispatcher_pair!(player_damage_dispatcher -> IPlayerDamageDispatcher @ SLOT_DAMAGE_DISPATCHER,
1218                 add_player_damage_handler(PlayerDamageHandler));
1219
1220dispatcher_pair!(player_stream_dispatcher -> IPlayerStreamDispatcher @ SLOT_STREAM_DISPATCHER,
1221                 add_player_stream_handler(PlayerStreamHandler));
1222dispatcher_pair!(player_shot_dispatcher -> IPlayerShotDispatcher @ SLOT_SHOT_DISPATCHER,
1223                 add_player_shot_handler(PlayerShotHandler));
1224dispatcher_pair!(player_change_dispatcher -> IPlayerChangeDispatcher @ SLOT_CHANGE_DISPATCHER,
1225                 add_player_change_handler(PlayerChangeHandler));
1226dispatcher_pair!(player_click_dispatcher -> IPlayerClickDispatcher @ SLOT_CLICK_DISPATCHER,
1227                 add_player_click_handler(PlayerClickHandler));
1228dispatcher_pair!(player_check_dispatcher -> IPlayerCheckDispatcher @ SLOT_CHECK_DISPATCHER,
1229                 add_player_check_handler(PlayerCheckHandler));
1230dispatcher_pair!(player_update_dispatcher -> IPlayerUpdateDispatcher @ SLOT_UPDATE_DISPATCHER,
1231                 add_player_update_handler(PlayerUpdateHandler));
1232
1233#[cfg(test)]
1234mod tests {
1235    use super::*;
1236
1237    #[test]
1238    fn slots_match_the_official_binaries() {
1239        // Dumped from `omp-server`: Core [8] getPlayers, PlayerPool [10]
1240        // getPlayerConnectDispatcher. MSVC drops one destructor slot.
1241        #[cfg(not(target_env = "msvc"))]
1242        {
1243            assert_eq!(SLOT_GET_PLAYERS, 8);
1244            assert_eq!(SLOT_CONNECT_DISPATCHER, 10);
1245        }
1246        #[cfg(target_env = "msvc")]
1247        {
1248            assert_eq!(SLOT_GET_PLAYERS, 7);
1249            assert_eq!(SLOT_CONNECT_DISPATCHER, 9);
1250        }
1251    }
1252
1253    #[test]
1254    fn the_other_dispatcher_slots_match_the_dump() {
1255        // From the `PlayerPool` vtable of the official `omp-server`:
1256        // [9] spawn, [10] connect, [12] text, [15] damage.
1257        #[cfg(not(target_env = "msvc"))]
1258        {
1259            assert_eq!(SLOT_SPAWN_DISPATCHER, 9);
1260            assert_eq!(SLOT_TEXT_DISPATCHER, 12);
1261            assert_eq!(SLOT_DAMAGE_DISPATCHER, 15);
1262        }
1263        #[cfg(target_env = "msvc")]
1264        {
1265            assert_eq!(SLOT_SPAWN_DISPATCHER, 8);
1266            assert_eq!(SLOT_TEXT_DISPATCHER, 11);
1267            assert_eq!(SLOT_DAMAGE_DISPATCHER, 14);
1268        }
1269    }
1270
1271    #[test]
1272    fn every_handler_vtable_has_the_slots_its_header_declares() {
1273        let pointer = std::mem::size_of::<*const ()>();
1274        assert_eq!(std::mem::size_of::<PlayerSpawnHandlerVTable>(), 2 * pointer);
1275        assert_eq!(std::mem::size_of::<PlayerTextHandlerVTable>(), 2 * pointer);
1276        assert_eq!(
1277            std::mem::size_of::<PlayerDamageHandlerVTable>(),
1278            3 * pointer
1279        );
1280        assert_eq!(std::mem::offset_of!(PlayerSpawnHandler, vtable), 0);
1281        assert_eq!(std::mem::offset_of!(PlayerTextHandler, vtable), 0);
1282        assert_eq!(std::mem::offset_of!(PlayerDamageHandler, vtable), 0);
1283    }
1284
1285    #[test]
1286    fn the_remaining_dispatcher_slots_match_the_dump() {
1287        // PlayerPool vtable of the official `omp-server`: [11] stream,
1288        // [13] shot, [14] change, [16] click, [17] check, [18] update.
1289        #[cfg(not(target_env = "msvc"))]
1290        let expected = [11, 13, 14, 16, 17, 18];
1291        #[cfg(target_env = "msvc")]
1292        let expected = [10, 12, 13, 15, 16, 17];
1293        assert_eq!(
1294            [
1295                SLOT_STREAM_DISPATCHER,
1296                SLOT_SHOT_DISPATCHER,
1297                SLOT_CHANGE_DISPATCHER,
1298                SLOT_CLICK_DISPATCHER,
1299                SLOT_CHECK_DISPATCHER,
1300                SLOT_UPDATE_DISPATCHER,
1301            ],
1302            expected
1303        );
1304    }
1305
1306    #[test]
1307    fn the_remaining_vtables_have_the_slots_their_headers_declare() {
1308        let p = std::mem::size_of::<*const ()>();
1309        assert_eq!(std::mem::size_of::<PlayerStreamHandlerVTable>(), 2 * p);
1310        assert_eq!(std::mem::size_of::<PlayerShotHandlerVTable>(), 5 * p);
1311        assert_eq!(std::mem::size_of::<PlayerChangeHandlerVTable>(), 5 * p);
1312        assert_eq!(std::mem::size_of::<PlayerClickHandlerVTable>(), 2 * p);
1313        assert_eq!(std::mem::size_of::<PlayerCheckHandlerVTable>(), p);
1314        assert_eq!(std::mem::size_of::<PlayerUpdateHandlerVTable>(), p);
1315    }
1316
1317    #[test]
1318    fn player_method_slots_match_the_dump() {
1319        // `Player` vtable of the official `omp-server`: [6] kick, [8] isBot,
1320        // [27] getName. MSVC drops the second destructor slot, and no
1321        // `IEntity` override (secondary base) sits before any of these, so the
1322        // shift is exactly one. Unlike the component classes, the Windows
1323        // server carries no RTTI for `Player`, so these cannot be re-derived
1324        // from the binary — `scripts/omp-vtable.py` derives them from the
1325        // headers with clang, and a running server proves them.
1326        #[cfg(not(target_env = "msvc"))]
1327        let expected = [6, 8, 27, 77, 78, 79, 80, 100];
1328        #[cfg(target_env = "msvc")]
1329        let expected = [5, 7, 26, 76, 77, 78, 79, 99];
1330
1331        // The plain accessors added later, same source, same shift.
1332        #[cfg(not(target_env = "msvc"))]
1333        assert_eq!(
1334            [
1335                SLOT_PLAYER_SET_DRUNK,
1336                SLOT_PLAYER_SET_CONTROLLABLE,
1337                SLOT_PLAYER_SET_WANTED,
1338                SLOT_PLAYER_GET_WANTED,
1339                SLOT_PLAYER_GET_MONEY,
1340                SLOT_PLAYER_GET_SKIN,
1341                SLOT_PLAYER_SET_WEATHER,
1342                SLOT_PLAYER_SET_INTERIOR,
1343                SLOT_PLAYER_GET_INTERIOR,
1344            ],
1345            [40, 46, 49, 50, 65, 98, 107, 118, 119]
1346        );
1347        #[cfg(target_env = "msvc")]
1348        assert_eq!(
1349            [
1350                SLOT_PLAYER_SET_DRUNK,
1351                SLOT_PLAYER_SET_CONTROLLABLE,
1352                SLOT_PLAYER_SET_WANTED,
1353                SLOT_PLAYER_GET_WANTED,
1354                SLOT_PLAYER_GET_MONEY,
1355                SLOT_PLAYER_GET_SKIN,
1356                SLOT_PLAYER_SET_WEATHER,
1357                SLOT_PLAYER_SET_INTERIOR,
1358                SLOT_PLAYER_GET_INTERIOR,
1359            ],
1360            [39, 45, 48, 49, 64, 97, 106, 117, 118]
1361        );
1362        assert_eq!(
1363            [
1364                SLOT_PLAYER_KICK,
1365                SLOT_PLAYER_IS_BOT,
1366                SLOT_PLAYER_GET_NAME,
1367                SLOT_PLAYER_SET_HEALTH,
1368                SLOT_PLAYER_GET_HEALTH,
1369                SLOT_PLAYER_SET_SCORE,
1370                SLOT_PLAYER_GET_SCORE,
1371                SLOT_PLAYER_SEND_MESSAGE,
1372            ],
1373            expected
1374        );
1375    }
1376
1377    #[test]
1378    fn the_entity_subobject_sits_where_iextensible_ends() {
1379        // clang's record layout for `IPlayer`: the `IEntity` base starts at 40
1380        // under Itanium and 56 under MSVC — the size of `IExtensible` on each,
1381        // the same number `OmpComponent` uses for its `IUIDProvider`.
1382        #[cfg(not(target_env = "msvc"))]
1383        assert_eq!(ENTITY_OFFSET, 40);
1384        #[cfg(target_env = "msvc")]
1385        assert_eq!(ENTITY_OFFSET, 56);
1386
1387        // `IEntity` declares no destructor, so both ABIs number it the same.
1388        assert_eq!(
1389            [
1390                SLOT_ENTITY_GET_POSITION,
1391                SLOT_ENTITY_SET_POSITION,
1392                SLOT_ENTITY_GET_VIRTUAL_WORLD,
1393                SLOT_ENTITY_SET_VIRTUAL_WORLD,
1394            ],
1395            [1, 2, 5, 6]
1396        );
1397    }
1398
1399    #[test]
1400    fn handler_layout_is_a_bare_vtable_pointer() {
1401        assert_eq!(std::mem::offset_of!(PlayerConnectHandler, vtable), 0);
1402        assert_eq!(
1403            std::mem::size_of::<PlayerConnectHandler>(),
1404            std::mem::size_of::<*const ()>()
1405        );
1406        assert_eq!(
1407            std::mem::size_of::<PlayerConnectHandlerVTable>(),
1408            4 * std::mem::size_of::<*const ()>(),
1409            "four slots: no virtual destructor in the header"
1410        );
1411    }
1412
1413    #[test]
1414    fn unknown_disconnect_reason_does_not_invent_a_variant() {
1415        assert_eq!(DisconnectReason::from_raw(1), DisconnectReason::Quit);
1416        assert_eq!(DisconnectReason::from_raw(99), DisconnectReason::Custom);
1417    }
1418}