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 `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}