use super::component::ICore;
use super::types::{Colour, StringView, UID, Vector3};
use super::vehicles::IVehicle;
use super::vtable::{call_vtable, call_vtable_small_struct, opaque, slots, virtual_fns};
slots! {
SLOT_GET_PLAYERS: usize = 8, 7;
SLOT_CONNECT_DISPATCHER: usize = 10, 9;
}
opaque! {
pub IPlayerPool;
}
slots! {
SLOT_PLAYER_KICK: usize = 6, 5;
SLOT_PLAYER_IS_BOT: usize = 8, 7;
SLOT_PLAYER_GET_NAME: usize = 27, 26;
SLOT_PLAYER_SET_HEALTH: usize = 77, 76;
SLOT_PLAYER_GET_HEALTH: usize = 78, 77;
pub(crate) ENTITY_OFFSET: isize = 40, 56;
PLAYER_POOL_OFFSET: isize = 40, 56;
}
pub(crate) const SLOT_POOL_GET: usize = 0;
pub(crate) const SLOT_POOL_BOUNDS: usize = 1;
const SLOT_GET_EXTENSION: usize = 0;
pub(crate) const SLOT_ENTITY_GET_ID: usize = 0;
pub(crate) const SLOT_ENTITY_GET_POSITION: usize = 1;
const SLOT_ENTITY_SET_POSITION: usize = 2;
pub(crate) const SLOT_ENTITY_GET_ROTATION: usize = 3;
pub(crate) const SLOT_ENTITY_SET_ROTATION: usize = 4;
const SLOT_ENTITY_GET_VIRTUAL_WORLD: usize = 5;
const SLOT_ENTITY_SET_VIRTUAL_WORLD: usize = 6;
slots! {
SLOT_PLAYER_SET_MONEY: usize = 62, 61;
SLOT_PLAYER_GIVE_MONEY: usize = 63, 62;
SLOT_PLAYER_SET_ARMOUR: usize = 81, 80;
SLOT_PLAYER_GET_ARMOUR: usize = 82, 81;
SLOT_PLAYER_SET_TEAM: usize = 95, 94;
SLOT_PLAYER_GET_TEAM: usize = 96, 95;
SLOT_PLAYER_SET_SKIN: usize = 97, 96;
SLOT_PLAYER_SET_DRUNK: usize = 40, 39;
SLOT_PLAYER_SET_WANTED: usize = 49, 48;
SLOT_PLAYER_GET_WANTED: usize = 50, 49;
SLOT_PLAYER_GET_MONEY: usize = 65, 64;
SLOT_PLAYER_GET_SKIN: usize = 98, 97;
SLOT_PLAYER_SET_WEATHER: usize = 107, 106;
SLOT_PLAYER_SET_INTERIOR: usize = 118, 117;
SLOT_PLAYER_GET_INTERIOR: usize = 119, 118;
SLOT_PLAYER_SET_CONTROLLABLE: usize = 46, 45;
SLOT_PLAYER_SET_SCORE: usize = 79, 78;
SLOT_PLAYER_GET_SCORE: usize = 80, 79;
SLOT_PLAYER_SEND_MESSAGE: usize = 100, 99;
}
opaque! {
pub IPlayer;
}
slots! {
SLOT_SPAWN_DISPATCHER: usize = 9, 8;
SLOT_TEXT_DISPATCHER: usize = 12, 11;
SLOT_DAMAGE_DISPATCHER: usize = 15, 14;
SLOT_STREAM_DISPATCHER: usize = 11, 10;
SLOT_SHOT_DISPATCHER: usize = 13, 12;
SLOT_CHANGE_DISPATCHER: usize = 14, 13;
SLOT_CLICK_DISPATCHER: usize = 16, 15;
SLOT_CHECK_DISPATCHER: usize = 17, 16;
SLOT_UPDATE_DISPATCHER: usize = 18, 17;
}
opaque! {
pub IPlayerConnectDispatcher;
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisconnectReason {
Timeout = 0,
Quit = 1,
Kicked = 2,
Custom = 3,
ModeEnd = 4,
}
impl DisconnectReason {
#[must_use]
pub fn from_raw(value: i32) -> Self {
match value {
0 => Self::Timeout,
1 => Self::Quit,
2 => Self::Kicked,
4 => Self::ModeEnd,
_ => Self::Custom,
}
}
}
#[cfg(not(target_env = "msvc"))]
#[repr(C)]
pub struct PlayerConnectHandlerVTable {
pub on_incoming_connection:
unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer, StringView, u16),
pub on_player_connect: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer),
pub on_player_disconnect: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer, i32),
pub on_player_client_init: unsafe extern "C" fn(*mut PlayerConnectHandler, *mut IPlayer),
}
#[cfg(target_env = "msvc")]
#[repr(C)]
pub struct PlayerConnectHandlerVTable {
pub on_incoming_connection:
unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer, StringView, u16),
pub on_player_connect: unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer),
pub on_player_disconnect:
unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer, i32),
pub on_player_client_init: unsafe extern "thiscall" fn(*mut PlayerConnectHandler, *mut IPlayer),
}
#[repr(C)]
pub struct PlayerConnectHandler {
vtable: *const PlayerConnectHandlerVTable,
}
unsafe impl Send for PlayerConnectHandler {}
unsafe impl Sync for PlayerConnectHandler {}
impl PlayerConnectHandler {
#[must_use]
pub fn new(vtable: *const PlayerConnectHandlerVTable) -> Self {
Self { vtable }
}
}
opaque! {
pub IObject;
pub IPlayerObject;
pub PlayerBulletData;
pub IPlayerSpawnDispatcher;
pub IPlayerTextDispatcher;
pub IPlayerDamageDispatcher;
pub IPlayerStreamDispatcher;
pub IPlayerShotDispatcher;
pub IPlayerChangeDispatcher;
pub IPlayerClickDispatcher;
pub IPlayerCheckDispatcher;
pub IPlayerUpdateDispatcher;
}
macro_rules! handler_vtable {
(
$(#[$meta:meta])*
$name:ident for $handler:ident {
$($field:ident: fn($($arg:ty),* $(,)?) $(-> $ret:ty)?),* $(,)?
}
) => {
$(#[$meta])*
#[cfg(not(target_env = "msvc"))]
#[repr(C)]
pub struct $name {
$(pub $field: unsafe extern "C" fn(*mut $handler, $($arg),*) $(-> $ret)?),*
}
$(#[$meta])*
#[cfg(target_env = "msvc")]
#[repr(C)]
pub struct $name {
$(pub $field: unsafe extern "thiscall" fn(*mut $handler, $($arg),*) $(-> $ret)?),*
}
#[doc = stringify!($name)]
#[repr(C)]
pub struct $handler {
vtable: *const $name,
}
unsafe impl Send for $handler {}
unsafe impl Sync for $handler {}
impl $handler {
#[must_use]
pub fn new(vtable: *const $name) -> Self {
Self { vtable }
}
}
};
}
handler_vtable! {
PlayerSpawnHandlerVTable for PlayerSpawnHandler {
on_player_request_spawn: fn(*mut IPlayer) -> bool,
on_player_spawn: fn(*mut IPlayer),
}
}
handler_vtable! {
PlayerTextHandlerVTable for PlayerTextHandler {
on_player_text: fn(*mut IPlayer, StringView) -> bool,
on_player_command_text: fn(*mut IPlayer, StringView) -> bool,
}
}
handler_vtable! {
PlayerDamageHandlerVTable for PlayerDamageHandler {
on_player_death: fn(*mut IPlayer, *mut IPlayer, i32),
on_player_take_damage: fn(*mut IPlayer, *mut IPlayer, f32, u32, i32),
on_player_give_damage: fn(*mut IPlayer, *mut IPlayer, f32, u32, i32),
}
}
#[must_use]
pub unsafe fn player_pool(core: *mut ICore) -> *mut IPlayerPool {
call_vtable!(
core.cast::<u8>(),
0,
SLOT_GET_PLAYERS,
() -> *mut IPlayerPool,
(),
std::ptr::null_mut()
)
}
#[must_use]
pub unsafe fn player_connect_dispatcher(pool: *mut IPlayerPool) -> *mut IPlayerConnectDispatcher {
#[cfg(not(target_env = "msvc"))]
type GetDispatcherFn = unsafe extern "C" fn(*mut u8) -> *mut IPlayerConnectDispatcher;
#[cfg(target_env = "msvc")]
type GetDispatcherFn = unsafe extern "thiscall" fn(*mut u8) -> *mut IPlayerConnectDispatcher;
let Some((this, f_ptr)) = (unsafe {
super::vtable::secondary_call_target_ptr(pool.cast::<u8>(), 0, SLOT_CONNECT_DISPATCHER)
}) else {
return std::ptr::null_mut();
};
let get_dispatcher: GetDispatcherFn = unsafe { std::mem::transmute(f_ptr) };
unsafe { get_dispatcher(this) }
}
pub unsafe fn add_player_connect_handler(
dispatcher: *mut IPlayerConnectDispatcher,
handler: *mut PlayerConnectHandler,
) -> bool {
call_vtable!(
dispatcher.cast::<u8>(),
0,
0,
(*mut PlayerConnectHandler, i8) -> bool,
(handler, 0),
false
)
}
handler_vtable! {
PlayerStreamHandlerVTable for PlayerStreamHandler {
on_player_stream_in: fn(*mut IPlayer, *mut IPlayer),
on_player_stream_out: fn(*mut IPlayer, *mut IPlayer),
}
}
handler_vtable! {
PlayerShotHandlerVTable for PlayerShotHandler {
on_player_shot_missed: fn(*mut IPlayer, *const PlayerBulletData) -> bool,
on_player_shot_player: fn(*mut IPlayer, *mut IPlayer, *const PlayerBulletData) -> bool,
on_player_shot_vehicle: fn(*mut IPlayer, *mut IVehicle, *const PlayerBulletData) -> bool,
on_player_shot_object: fn(*mut IPlayer, *mut IObject, *const PlayerBulletData) -> bool,
on_player_shot_player_object:
fn(*mut IPlayer, *mut IPlayerObject, *const PlayerBulletData) -> bool,
}
}
handler_vtable! {
PlayerChangeHandlerVTable for PlayerChangeHandler {
on_player_score_change: fn(*mut IPlayer, i32),
on_player_name_change: fn(*mut IPlayer, StringView),
on_player_interior_change: fn(*mut IPlayer, u32, u32),
on_player_state_change: fn(*mut IPlayer, i32, i32),
on_player_key_state_change: fn(*mut IPlayer, u32, u32),
}
}
handler_vtable! {
PlayerClickHandlerVTable for PlayerClickHandler {
on_player_click_map: fn(*mut IPlayer, Vector3),
on_player_click_player: fn(*mut IPlayer, *mut IPlayer, i32),
}
}
handler_vtable! {
PlayerCheckHandlerVTable for PlayerCheckHandler {
on_client_check_response: fn(*mut IPlayer, i32, i32, i32),
}
}
handler_vtable! {
PlayerUpdateHandlerVTable for PlayerUpdateHandler {
on_player_update: fn(*mut IPlayer, i64) -> bool,
}
}
virtual_fns! {
pub fn player_kick(player: IPlayer) = [0, SLOT_PLAYER_KICK];
#[must_use]
pub fn player_is_bot(player: IPlayer) -> bool = [0, SLOT_PLAYER_IS_BOT] or false;
}
#[must_use]
pub unsafe fn player_name(player: *mut IPlayer) -> Option<String> {
let view = call_vtable_small_struct!(
player.cast::<u8>(),
0,
SLOT_PLAYER_GET_NAME,
StringView,
StringView::EMPTY
)?;
unsafe { view.to_owned_string() }
}
virtual_fns! {
#[must_use]
pub fn player_health(player: IPlayer) -> f32 = [0, SLOT_PLAYER_GET_HEALTH] or 0.0;
pub fn player_set_health(player: IPlayer, health: f32) = [0, SLOT_PLAYER_SET_HEALTH];
#[must_use]
pub fn player_score(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_SCORE] or 0;
pub fn player_set_score(player: IPlayer, score: i32) = [0, SLOT_PLAYER_SET_SCORE];
#[must_use]
pub fn player_position(player: IPlayer) -> Vector3 = [ENTITY_OFFSET, SLOT_ENTITY_GET_POSITION] or Vector3::ZERO;
pub fn player_set_position(player: IPlayer, position: Vector3) = [ENTITY_OFFSET, SLOT_ENTITY_SET_POSITION];
#[must_use]
pub fn player_virtual_world(player: IPlayer) -> i32 = [ENTITY_OFFSET, SLOT_ENTITY_GET_VIRTUAL_WORLD] or 0;
pub fn player_set_virtual_world(player: IPlayer, world: i32) = [ENTITY_OFFSET, SLOT_ENTITY_SET_VIRTUAL_WORLD];
pub fn player_set_money(player: IPlayer, amount: i32) = [0, SLOT_PLAYER_SET_MONEY];
pub fn player_give_money(player: IPlayer, amount: i32) = [0, SLOT_PLAYER_GIVE_MONEY];
pub fn player_set_team(player: IPlayer, team: i32) = [0, SLOT_PLAYER_SET_TEAM];
#[must_use]
pub fn player_team(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_TEAM] or 0;
pub fn player_set_armour(player: IPlayer, armour: f32) = [0, SLOT_PLAYER_SET_ARMOUR];
#[must_use]
pub fn player_armour(player: IPlayer) -> f32 = [0, SLOT_PLAYER_GET_ARMOUR] or 0.0;
pub fn player_set_skin(player: IPlayer, skin: i32, send: bool) = [0, SLOT_PLAYER_SET_SKIN];
#[must_use]
pub fn player_id(player: IPlayer) -> i32 = [ENTITY_OFFSET, SLOT_ENTITY_GET_ID] or -1;
#[must_use]
pub fn player_by_id(pool: IPlayerPool, id: i32) -> *mut IPlayer = [PLAYER_POOL_OFFSET, SLOT_POOL_GET] or std::ptr::null_mut();
#[must_use]
pub fn player_money(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_MONEY] or 0;
#[must_use]
pub fn player_skin(player: IPlayer) -> i32 = [0, SLOT_PLAYER_GET_SKIN] or 0;
pub fn player_set_wanted_level(player: IPlayer, level: u32) = [0, SLOT_PLAYER_SET_WANTED];
#[must_use]
pub fn player_wanted_level(player: IPlayer) -> u32 = [0, SLOT_PLAYER_GET_WANTED] or 0;
pub fn player_set_interior(player: IPlayer, interior: u32) = [0, SLOT_PLAYER_SET_INTERIOR];
#[must_use]
pub fn player_interior(player: IPlayer) -> u32 = [0, SLOT_PLAYER_GET_INTERIOR] or 0;
pub fn player_set_weather(player: IPlayer, weather: i32) = [0, SLOT_PLAYER_SET_WEATHER];
pub fn player_set_drunk_level(player: IPlayer, level: i32) = [0, SLOT_PLAYER_SET_DRUNK];
pub fn player_set_controllable(player: IPlayer, controllable: bool) = [0, SLOT_PLAYER_SET_CONTROLLABLE];
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PoolBounds {
pub first: usize,
pub last: usize,
}
#[must_use]
pub unsafe fn pool_bounds(pool: *mut u8, offset: isize) -> PoolBounds {
#[cfg(not(target_env = "msvc"))]
type BoundsFn = unsafe extern "C" fn(*mut u8) -> PoolBounds;
#[cfg(target_env = "msvc")]
type BoundsFn = unsafe extern "thiscall" fn(*mut u8, *mut PoolBounds) -> *mut PoolBounds;
let empty = PoolBounds { first: 1, last: 0 };
let Some((this, f_ptr)) =
(unsafe { super::vtable::secondary_call_target_ptr(pool, offset, SLOT_POOL_BOUNDS) })
else {
return empty;
};
let bounds: BoundsFn = unsafe { std::mem::transmute(f_ptr) };
#[cfg(not(target_env = "msvc"))]
let result = unsafe { bounds(this) };
#[cfg(target_env = "msvc")]
let result = {
let mut out = empty;
unsafe { bounds(this, &raw mut out) };
out
};
result
}
#[must_use]
pub unsafe fn all_players(pool: *mut IPlayerPool) -> Vec<*mut IPlayer> {
let bounds = unsafe { pool_bounds(pool.cast::<u8>(), PLAYER_POOL_OFFSET) };
let mut found = Vec::new();
for id in bounds.first..=bounds.last {
let Ok(id) = i32::try_from(id) else { break };
let player = unsafe { player_by_id(pool, id) };
if !player.is_null() {
found.push(player);
}
}
found
}
#[must_use]
pub unsafe fn player_extension(player: *mut IPlayer, uid: UID) -> *mut u8 {
let attached = unsafe { super::extensions::extension(player.cast::<u8>(), uid) };
if !attached.is_null() {
return attached;
}
call_vtable!(
player.cast::<u8>(),
0,
SLOT_GET_EXTENSION,
(UID) -> *mut u8,
(uid),
std::ptr::null_mut()
)
}
pub unsafe fn player_send_message(player: *mut IPlayer, colour: Colour, text: &str) {
let text = StringView::of(text);
call_vtable!(
player.cast::<u8>(),
0,
SLOT_PLAYER_SEND_MESSAGE,
(*const Colour, StringView) -> (),
(&raw const colour, text),
()
)
}
macro_rules! dispatcher_pair {
($getter:ident -> $dispatcher:ident @ $slot:ident, $adder:ident($handler:ident)) => {
#[must_use]
pub unsafe fn $getter(pool: *mut IPlayerPool) -> *mut $dispatcher {
#[cfg(not(target_env = "msvc"))]
type GetFn = unsafe extern "C" fn(*mut u8) -> *mut $dispatcher;
#[cfg(target_env = "msvc")]
type GetFn = unsafe extern "thiscall" fn(*mut u8) -> *mut $dispatcher;
let Some((this, f_ptr)) =
(unsafe { super::vtable::secondary_call_target_ptr(pool.cast::<u8>(), 0, $slot) })
else {
return std::ptr::null_mut();
};
let get: GetFn = unsafe { std::mem::transmute(f_ptr) };
unsafe { get(this) }
}
pub unsafe fn $adder(dispatcher: *mut $dispatcher, handler: *mut $handler) -> bool {
call_vtable!(
dispatcher.cast::<u8>(),
0,
0,
(*mut $handler, i8) -> bool,
(handler, 0),
false
)
}
};
}
dispatcher_pair!(player_spawn_dispatcher -> IPlayerSpawnDispatcher @ SLOT_SPAWN_DISPATCHER,
add_player_spawn_handler(PlayerSpawnHandler));
dispatcher_pair!(player_text_dispatcher -> IPlayerTextDispatcher @ SLOT_TEXT_DISPATCHER,
add_player_text_handler(PlayerTextHandler));
dispatcher_pair!(player_damage_dispatcher -> IPlayerDamageDispatcher @ SLOT_DAMAGE_DISPATCHER,
add_player_damage_handler(PlayerDamageHandler));
dispatcher_pair!(player_stream_dispatcher -> IPlayerStreamDispatcher @ SLOT_STREAM_DISPATCHER,
add_player_stream_handler(PlayerStreamHandler));
dispatcher_pair!(player_shot_dispatcher -> IPlayerShotDispatcher @ SLOT_SHOT_DISPATCHER,
add_player_shot_handler(PlayerShotHandler));
dispatcher_pair!(player_change_dispatcher -> IPlayerChangeDispatcher @ SLOT_CHANGE_DISPATCHER,
add_player_change_handler(PlayerChangeHandler));
dispatcher_pair!(player_click_dispatcher -> IPlayerClickDispatcher @ SLOT_CLICK_DISPATCHER,
add_player_click_handler(PlayerClickHandler));
dispatcher_pair!(player_check_dispatcher -> IPlayerCheckDispatcher @ SLOT_CHECK_DISPATCHER,
add_player_check_handler(PlayerCheckHandler));
dispatcher_pair!(player_update_dispatcher -> IPlayerUpdateDispatcher @ SLOT_UPDATE_DISPATCHER,
add_player_update_handler(PlayerUpdateHandler));
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slots_match_the_official_binaries() {
#[cfg(not(target_env = "msvc"))]
{
assert_eq!(SLOT_GET_PLAYERS, 8);
assert_eq!(SLOT_CONNECT_DISPATCHER, 10);
}
#[cfg(target_env = "msvc")]
{
assert_eq!(SLOT_GET_PLAYERS, 7);
assert_eq!(SLOT_CONNECT_DISPATCHER, 9);
}
}
#[test]
fn the_other_dispatcher_slots_match_the_dump() {
#[cfg(not(target_env = "msvc"))]
{
assert_eq!(SLOT_SPAWN_DISPATCHER, 9);
assert_eq!(SLOT_TEXT_DISPATCHER, 12);
assert_eq!(SLOT_DAMAGE_DISPATCHER, 15);
}
#[cfg(target_env = "msvc")]
{
assert_eq!(SLOT_SPAWN_DISPATCHER, 8);
assert_eq!(SLOT_TEXT_DISPATCHER, 11);
assert_eq!(SLOT_DAMAGE_DISPATCHER, 14);
}
}
#[test]
fn every_handler_vtable_has_the_slots_its_header_declares() {
let pointer = std::mem::size_of::<*const ()>();
assert_eq!(std::mem::size_of::<PlayerSpawnHandlerVTable>(), 2 * pointer);
assert_eq!(std::mem::size_of::<PlayerTextHandlerVTable>(), 2 * pointer);
assert_eq!(
std::mem::size_of::<PlayerDamageHandlerVTable>(),
3 * pointer
);
assert_eq!(std::mem::offset_of!(PlayerSpawnHandler, vtable), 0);
assert_eq!(std::mem::offset_of!(PlayerTextHandler, vtable), 0);
assert_eq!(std::mem::offset_of!(PlayerDamageHandler, vtable), 0);
}
#[test]
fn the_remaining_dispatcher_slots_match_the_dump() {
#[cfg(not(target_env = "msvc"))]
let expected = [11, 13, 14, 16, 17, 18];
#[cfg(target_env = "msvc")]
let expected = [10, 12, 13, 15, 16, 17];
assert_eq!(
[
SLOT_STREAM_DISPATCHER,
SLOT_SHOT_DISPATCHER,
SLOT_CHANGE_DISPATCHER,
SLOT_CLICK_DISPATCHER,
SLOT_CHECK_DISPATCHER,
SLOT_UPDATE_DISPATCHER,
],
expected
);
}
#[test]
fn the_remaining_vtables_have_the_slots_their_headers_declare() {
let p = std::mem::size_of::<*const ()>();
assert_eq!(std::mem::size_of::<PlayerStreamHandlerVTable>(), 2 * p);
assert_eq!(std::mem::size_of::<PlayerShotHandlerVTable>(), 5 * p);
assert_eq!(std::mem::size_of::<PlayerChangeHandlerVTable>(), 5 * p);
assert_eq!(std::mem::size_of::<PlayerClickHandlerVTable>(), 2 * p);
assert_eq!(std::mem::size_of::<PlayerCheckHandlerVTable>(), p);
assert_eq!(std::mem::size_of::<PlayerUpdateHandlerVTable>(), p);
}
#[test]
fn player_method_slots_match_the_dump() {
#[cfg(not(target_env = "msvc"))]
let expected = [6, 8, 27, 77, 78, 79, 80, 100];
#[cfg(target_env = "msvc")]
let expected = [5, 7, 26, 76, 77, 78, 79, 99];
#[cfg(not(target_env = "msvc"))]
assert_eq!(
[
SLOT_PLAYER_SET_DRUNK,
SLOT_PLAYER_SET_CONTROLLABLE,
SLOT_PLAYER_SET_WANTED,
SLOT_PLAYER_GET_WANTED,
SLOT_PLAYER_GET_MONEY,
SLOT_PLAYER_GET_SKIN,
SLOT_PLAYER_SET_WEATHER,
SLOT_PLAYER_SET_INTERIOR,
SLOT_PLAYER_GET_INTERIOR,
],
[40, 46, 49, 50, 65, 98, 107, 118, 119]
);
#[cfg(target_env = "msvc")]
assert_eq!(
[
SLOT_PLAYER_SET_DRUNK,
SLOT_PLAYER_SET_CONTROLLABLE,
SLOT_PLAYER_SET_WANTED,
SLOT_PLAYER_GET_WANTED,
SLOT_PLAYER_GET_MONEY,
SLOT_PLAYER_GET_SKIN,
SLOT_PLAYER_SET_WEATHER,
SLOT_PLAYER_SET_INTERIOR,
SLOT_PLAYER_GET_INTERIOR,
],
[39, 45, 48, 49, 64, 97, 106, 117, 118]
);
assert_eq!(
[
SLOT_PLAYER_KICK,
SLOT_PLAYER_IS_BOT,
SLOT_PLAYER_GET_NAME,
SLOT_PLAYER_SET_HEALTH,
SLOT_PLAYER_GET_HEALTH,
SLOT_PLAYER_SET_SCORE,
SLOT_PLAYER_GET_SCORE,
SLOT_PLAYER_SEND_MESSAGE,
],
expected
);
}
#[test]
fn the_entity_subobject_sits_where_iextensible_ends() {
#[cfg(not(target_env = "msvc"))]
assert_eq!(ENTITY_OFFSET, 40);
#[cfg(target_env = "msvc")]
assert_eq!(ENTITY_OFFSET, 56);
assert_eq!(
[
SLOT_ENTITY_GET_POSITION,
SLOT_ENTITY_SET_POSITION,
SLOT_ENTITY_GET_VIRTUAL_WORLD,
SLOT_ENTITY_SET_VIRTUAL_WORLD,
],
[1, 2, 5, 6]
);
}
#[test]
fn handler_layout_is_a_bare_vtable_pointer() {
assert_eq!(std::mem::offset_of!(PlayerConnectHandler, vtable), 0);
assert_eq!(
std::mem::size_of::<PlayerConnectHandler>(),
std::mem::size_of::<*const ()>()
);
assert_eq!(
std::mem::size_of::<PlayerConnectHandlerVTable>(),
4 * std::mem::size_of::<*const ()>(),
"four slots: no virtual destructor in the header"
);
}
#[test]
fn unknown_disconnect_reason_does_not_invent_a_variant() {
assert_eq!(DisconnectReason::from_raw(1), DisconnectReason::Quit);
assert_eq!(DisconnectReason::from_raw(99), DisconnectReason::Custom);
}
}