1use super::component::ICore;
39use super::types::{Colour, StringView, UID, Vector3};
40use super::vehicles::IVehicle;
41use super::vtable::call_vtable;
42
43#[cfg(not(target_env = "msvc"))]
45const SLOT_GET_PLAYERS: usize = 8;
46#[cfg(target_env = "msvc")]
47const SLOT_GET_PLAYERS: usize = 7;
48
49#[cfg(not(target_env = "msvc"))]
51const SLOT_CONNECT_DISPATCHER: usize = 10;
52#[cfg(target_env = "msvc")]
53const SLOT_CONNECT_DISPATCHER: usize = 9;
54
55#[repr(C)]
57pub struct IPlayerPool {
58 _opaque: [u8; 0],
59}
60
61#[cfg(not(target_env = "msvc"))]
63const SLOT_PLAYER_KICK: usize = 6;
64#[cfg(target_env = "msvc")]
65const SLOT_PLAYER_KICK: usize = 5;
66
67#[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#[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#[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#[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#[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#[cfg(not(target_env = "msvc"))]
109const PLAYER_POOL_OFFSET: isize = 40;
110#[cfg(target_env = "msvc")]
111const PLAYER_POOL_OFFSET: isize = 56;
112
113pub(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
119const SLOT_GET_EXTENSION: usize = 0;
122
123pub(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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[repr(C)]
242pub struct IPlayer {
243 _opaque: [u8; 0],
244}
245
246#[cfg(not(target_env = "msvc"))]
248const SLOT_SPAWN_DISPATCHER: usize = 9;
249#[cfg(target_env = "msvc")]
250const SLOT_SPAWN_DISPATCHER: usize = 8;
251
252#[cfg(not(target_env = "msvc"))]
254const SLOT_TEXT_DISPATCHER: usize = 12;
255#[cfg(target_env = "msvc")]
256const SLOT_TEXT_DISPATCHER: usize = 11;
257
258#[cfg(not(target_env = "msvc"))]
260const SLOT_DAMAGE_DISPATCHER: usize = 15;
261#[cfg(target_env = "msvc")]
262const SLOT_DAMAGE_DISPATCHER: usize = 14;
263
264#[cfg(not(target_env = "msvc"))]
266const SLOT_STREAM_DISPATCHER: usize = 11;
267#[cfg(target_env = "msvc")]
268const SLOT_STREAM_DISPATCHER: usize = 10;
269
270#[cfg(not(target_env = "msvc"))]
272const SLOT_SHOT_DISPATCHER: usize = 13;
273#[cfg(target_env = "msvc")]
274const SLOT_SHOT_DISPATCHER: usize = 12;
275
276#[cfg(not(target_env = "msvc"))]
278const SLOT_CHANGE_DISPATCHER: usize = 14;
279#[cfg(target_env = "msvc")]
280const SLOT_CHANGE_DISPATCHER: usize = 13;
281
282#[cfg(not(target_env = "msvc"))]
284const SLOT_CLICK_DISPATCHER: usize = 16;
285#[cfg(target_env = "msvc")]
286const SLOT_CLICK_DISPATCHER: usize = 15;
287
288#[cfg(not(target_env = "msvc"))]
290const SLOT_CHECK_DISPATCHER: usize = 17;
291#[cfg(target_env = "msvc")]
292const SLOT_CHECK_DISPATCHER: usize = 16;
293
294#[cfg(not(target_env = "msvc"))]
296const SLOT_UPDATE_DISPATCHER: usize = 18;
297#[cfg(target_env = "msvc")]
298const SLOT_UPDATE_DISPATCHER: usize = 17;
299
300#[repr(C)]
302pub struct IPlayerConnectDispatcher {
303 _opaque: [u8; 0],
304}
305
306#[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 #[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#[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#[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#[repr(C)]
362pub struct PlayerConnectHandler {
363 vtable: *const PlayerConnectHandlerVTable,
364}
365
366unsafe impl Send for PlayerConnectHandler {}
368unsafe impl Sync for PlayerConnectHandler {}
369
370impl PlayerConnectHandler {
371 #[must_use]
373 pub fn new(vtable: *const PlayerConnectHandlerVTable) -> Self {
374 Self { vtable }
375 }
376}
377
378#[repr(C)]
381pub struct IObject {
382 _opaque: [u8; 0],
383}
384
385#[repr(C)]
387pub struct IPlayerObject {
388 _opaque: [u8; 0],
389}
390
391#[repr(C)]
394pub struct PlayerBulletData {
395 _opaque: [u8; 0],
396}
397
398#[repr(C)]
400pub struct IPlayerSpawnDispatcher {
401 _opaque: [u8; 0],
402}
403
404#[repr(C)]
406pub struct IPlayerTextDispatcher {
407 _opaque: [u8; 0],
408}
409
410#[repr(C)]
412pub struct IPlayerDamageDispatcher {
413 _opaque: [u8; 0],
414}
415
416#[repr(C)]
418pub struct IPlayerStreamDispatcher {
419 _opaque: [u8; 0],
420}
421
422#[repr(C)]
424pub struct IPlayerShotDispatcher {
425 _opaque: [u8; 0],
426}
427
428#[repr(C)]
430pub struct IPlayerChangeDispatcher {
431 _opaque: [u8; 0],
432}
433
434#[repr(C)]
436pub struct IPlayerClickDispatcher {
437 _opaque: [u8; 0],
438}
439
440#[repr(C)]
442pub struct IPlayerCheckDispatcher {
443 _opaque: [u8; 0],
444}
445
446#[repr(C)]
448pub struct IPlayerUpdateDispatcher {
449 _opaque: [u8; 0],
450}
451
452macro_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 #[doc = stringify!($name)]
479 #[repr(C)]
483 pub struct $handler {
484 vtable: *const $name,
485 }
486
487 unsafe impl Send for $handler {}
489 unsafe impl Sync for $handler {}
490
491 impl $handler {
492 #[must_use]
494 pub fn new(vtable: *const $name) -> Self {
495 Self { vtable }
496 }
497 }
498 };
499}
500
501handler_vtable! {
502 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 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 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#[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#[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
566pub 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 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 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 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 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 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 PlayerCheckHandlerVTable for PlayerCheckHandler {
639 on_client_check_response: fn(*mut IPlayer, i32, i32, i32),
640 }
641}
642
643handler_vtable! {
644 PlayerUpdateHandlerVTable for PlayerUpdateHandler {
650 on_player_update: fn(*mut IPlayer, i64) -> bool,
651 }
652}
653
654pub unsafe fn player_kick(player: *mut IPlayer) {
660 call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_KICK, () -> (), (), ())
661}
662
663#[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#[must_use]
684pub unsafe fn player_name(player: *mut IPlayer) -> Option<String> {
685 #[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#[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
726pub 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#[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
754pub 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#[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
806pub 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#[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
837pub 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
852unsafe fn player_set_i32(player: *mut IPlayer, slot: usize, value: i32) {
854 call_vtable!(player.cast::<u8>(), 0, slot, (i32) -> (), (value), ())
855}
856
857unsafe fn player_get_i32(player: *mut IPlayer, slot: usize) -> i32 {
859 call_vtable!(player.cast::<u8>(), 0, slot, () -> i32, (), 0)
860}
861
862pub unsafe fn player_set_money(player: *mut IPlayer, amount: i32) {
867 unsafe { player_set_i32(player, SLOT_PLAYER_SET_MONEY, amount) };
868}
869
870pub unsafe fn player_give_money(player: *mut IPlayer, amount: i32) {
875 unsafe { player_set_i32(player, SLOT_PLAYER_GIVE_MONEY, amount) };
876}
877
878pub unsafe fn player_set_team(player: *mut IPlayer, team: i32) {
883 unsafe { player_set_i32(player, SLOT_PLAYER_SET_TEAM, team) };
884}
885
886#[must_use]
891pub unsafe fn player_team(player: *mut IPlayer) -> i32 {
892 unsafe { player_get_i32(player, SLOT_PLAYER_GET_TEAM) }
893}
894
895pub 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#[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
912pub 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#[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#[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#[must_use]
954pub unsafe fn player_money(player: *mut IPlayer) -> i32 {
955 unsafe { player_get_i32(player, SLOT_PLAYER_GET_MONEY) }
956}
957
958#[must_use]
963pub unsafe fn player_skin(player: *mut IPlayer) -> i32 {
964 unsafe { player_get_i32(player, SLOT_PLAYER_GET_SKIN) }
965}
966
967pub 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#[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
984pub 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#[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
1001pub unsafe fn player_set_weather(player: *mut IPlayer, weather: i32) {
1006 unsafe { player_set_i32(player, SLOT_PLAYER_SET_WEATHER, weather) };
1007}
1008
1009pub unsafe fn player_set_drunk_level(player: *mut IPlayer, level: i32) {
1014 unsafe { player_set_i32(player, SLOT_PLAYER_SET_DRUNK, level) };
1015}
1016
1017pub 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#[repr(C)]
1035#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1036pub struct PoolBounds {
1037 pub first: usize,
1038 pub last: usize,
1039}
1040
1041#[must_use]
1052pub unsafe fn pool_bounds(pool: *mut u8, offset: isize) -> PoolBounds {
1053 #[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 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#[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#[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
1141pub 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
1165macro_rules! dispatcher_pair {
1167 ($getter:ident -> $dispatcher:ident @ $slot:ident, $adder:ident($handler:ident)) => {
1168 #[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 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 #[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 #[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 #[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 #[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 #[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 #[cfg(not(target_env = "msvc"))]
1383 assert_eq!(ENTITY_OFFSET, 40);
1384 #[cfg(target_env = "msvc")]
1385 assert_eq!(ENTITY_OFFSET, 56);
1386
1387 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}