use super::component::ICore;
use super::types::{Colour, StringView, UID, Vector3};
use super::vehicles::IVehicle;
use super::vtable::call_vtable;
#[cfg(not(target_env = "msvc"))]
const SLOT_GET_PLAYERS: usize = 8;
#[cfg(target_env = "msvc")]
const SLOT_GET_PLAYERS: usize = 7;
#[cfg(not(target_env = "msvc"))]
const SLOT_CONNECT_DISPATCHER: usize = 10;
#[cfg(target_env = "msvc")]
const SLOT_CONNECT_DISPATCHER: usize = 9;
#[repr(C)]
pub struct IPlayerPool {
_opaque: [u8; 0],
}
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_KICK: usize = 6;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_KICK: usize = 5;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_IS_BOT: usize = 8;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_IS_BOT: usize = 7;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_NAME: usize = 27;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_NAME: usize = 26;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_HEALTH: usize = 77;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_HEALTH: usize = 76;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_HEALTH: usize = 78;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_HEALTH: usize = 77;
#[cfg(not(target_env = "msvc"))]
pub(crate) const ENTITY_OFFSET: isize = 40;
#[cfg(target_env = "msvc")]
pub(crate) const ENTITY_OFFSET: isize = 56;
#[cfg(not(target_env = "msvc"))]
const PLAYER_POOL_OFFSET: isize = 40;
#[cfg(target_env = "msvc")]
const PLAYER_POOL_OFFSET: isize = 56;
pub(crate) const SLOT_POOL_GET_PUB: usize = 0;
const SLOT_POOL_GET: usize = SLOT_POOL_GET_PUB;
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;
const SLOT_ENTITY_GET_VIRTUAL_WORLD: usize = 5;
const SLOT_ENTITY_SET_VIRTUAL_WORLD: usize = 6;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_MONEY: usize = 62;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_MONEY: usize = 61;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GIVE_MONEY: usize = 63;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GIVE_MONEY: usize = 62;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_ARMOUR: usize = 81;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_ARMOUR: usize = 80;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_ARMOUR: usize = 82;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_ARMOUR: usize = 81;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_TEAM: usize = 95;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_TEAM: usize = 94;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_TEAM: usize = 96;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_TEAM: usize = 95;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_SKIN: usize = 97;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_SKIN: usize = 96;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_DRUNK: usize = 40;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_DRUNK: usize = 39;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_WANTED: usize = 49;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_WANTED: usize = 48;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_WANTED: usize = 50;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_WANTED: usize = 49;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_MONEY: usize = 65;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_MONEY: usize = 64;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_SKIN: usize = 98;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_SKIN: usize = 97;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_WEATHER: usize = 107;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_WEATHER: usize = 106;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_INTERIOR: usize = 118;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_INTERIOR: usize = 117;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_INTERIOR: usize = 119;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_INTERIOR: usize = 118;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_CONTROLLABLE: usize = 46;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_CONTROLLABLE: usize = 45;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SET_SCORE: usize = 79;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SET_SCORE: usize = 78;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_GET_SCORE: usize = 80;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_GET_SCORE: usize = 79;
#[cfg(not(target_env = "msvc"))]
const SLOT_PLAYER_SEND_MESSAGE: usize = 100;
#[cfg(target_env = "msvc")]
const SLOT_PLAYER_SEND_MESSAGE: usize = 99;
#[repr(C)]
pub struct IPlayer {
_opaque: [u8; 0],
}
#[cfg(not(target_env = "msvc"))]
const SLOT_SPAWN_DISPATCHER: usize = 9;
#[cfg(target_env = "msvc")]
const SLOT_SPAWN_DISPATCHER: usize = 8;
#[cfg(not(target_env = "msvc"))]
const SLOT_TEXT_DISPATCHER: usize = 12;
#[cfg(target_env = "msvc")]
const SLOT_TEXT_DISPATCHER: usize = 11;
#[cfg(not(target_env = "msvc"))]
const SLOT_DAMAGE_DISPATCHER: usize = 15;
#[cfg(target_env = "msvc")]
const SLOT_DAMAGE_DISPATCHER: usize = 14;
#[cfg(not(target_env = "msvc"))]
const SLOT_STREAM_DISPATCHER: usize = 11;
#[cfg(target_env = "msvc")]
const SLOT_STREAM_DISPATCHER: usize = 10;
#[cfg(not(target_env = "msvc"))]
const SLOT_SHOT_DISPATCHER: usize = 13;
#[cfg(target_env = "msvc")]
const SLOT_SHOT_DISPATCHER: usize = 12;
#[cfg(not(target_env = "msvc"))]
const SLOT_CHANGE_DISPATCHER: usize = 14;
#[cfg(target_env = "msvc")]
const SLOT_CHANGE_DISPATCHER: usize = 13;
#[cfg(not(target_env = "msvc"))]
const SLOT_CLICK_DISPATCHER: usize = 16;
#[cfg(target_env = "msvc")]
const SLOT_CLICK_DISPATCHER: usize = 15;
#[cfg(not(target_env = "msvc"))]
const SLOT_CHECK_DISPATCHER: usize = 17;
#[cfg(target_env = "msvc")]
const SLOT_CHECK_DISPATCHER: usize = 16;
#[cfg(not(target_env = "msvc"))]
const SLOT_UPDATE_DISPATCHER: usize = 18;
#[cfg(target_env = "msvc")]
const SLOT_UPDATE_DISPATCHER: usize = 17;
#[repr(C)]
pub struct IPlayerConnectDispatcher {
_opaque: [u8; 0],
}
#[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 }
}
}
#[repr(C)]
pub struct IObject {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerObject {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct PlayerBulletData {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerSpawnDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerTextDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerDamageDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerStreamDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerShotDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerChangeDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerClickDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerCheckDispatcher {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct IPlayerUpdateDispatcher {
_opaque: [u8; 0],
}
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 {
#[cfg(not(target_env = "msvc"))]
type AddFn = unsafe extern "C" fn(*mut u8, *mut PlayerConnectHandler, i8) -> bool;
#[cfg(target_env = "msvc")]
type AddFn = unsafe extern "thiscall" fn(*mut u8, *mut PlayerConnectHandler, i8) -> bool;
let Some((this, f_ptr)) =
(unsafe { super::vtable::secondary_call_target_ptr(dispatcher.cast::<u8>(), 0, 0) })
else {
return false;
};
let add: AddFn = unsafe { std::mem::transmute(f_ptr) };
unsafe { add(this, handler, 0) }
}
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,
}
}
pub unsafe fn player_kick(player: *mut IPlayer) {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_KICK, () -> (), (), ())
}
#[must_use]
pub unsafe fn player_is_bot(player: *mut IPlayer) -> bool {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_IS_BOT, () -> bool, (), false)
}
#[must_use]
pub unsafe fn player_name(player: *mut IPlayer) -> Option<String> {
#[cfg(not(target_env = "msvc"))]
type GetNameFn = unsafe extern "C" fn(*mut u8) -> StringView;
#[cfg(target_env = "msvc")]
type GetNameFn = unsafe extern "thiscall" fn(*mut u8, *mut StringView) -> *mut StringView;
let (this, f_ptr) = unsafe {
super::vtable::secondary_call_target_ptr(player.cast::<u8>(), 0, SLOT_PLAYER_GET_NAME)
}?;
let get_name: GetNameFn = unsafe { std::mem::transmute(f_ptr) };
#[cfg(not(target_env = "msvc"))]
let view = unsafe { get_name(this) };
#[cfg(target_env = "msvc")]
let view = {
let mut view = StringView {
data: std::ptr::null(),
len: 0,
};
unsafe { get_name(this, &raw mut view) };
view
};
if view.data.is_null() || view.len == 0 {
return None;
}
let bytes = unsafe { std::slice::from_raw_parts(view.data, view.len) };
std::str::from_utf8(bytes).ok().map(String::from)
}
#[must_use]
pub unsafe fn player_health(player: *mut IPlayer) -> f32 {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_GET_HEALTH, () -> f32, (), 0.0)
}
pub unsafe fn player_set_health(player: *mut IPlayer, health: f32) {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_SET_HEALTH, (f32) -> (), (health), ())
}
#[must_use]
pub unsafe fn player_score(player: *mut IPlayer) -> i32 {
#[cfg(not(target_env = "msvc"))]
type GetScoreFn = unsafe extern "C" fn(*mut u8) -> i32;
#[cfg(target_env = "msvc")]
type GetScoreFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
let Some((this, f_ptr)) = (unsafe {
super::vtable::secondary_call_target_ptr(player.cast::<u8>(), 0, SLOT_PLAYER_GET_SCORE)
}) else {
return 0;
};
let get_score: GetScoreFn = unsafe { std::mem::transmute(f_ptr) };
unsafe { get_score(this) }
}
pub unsafe fn player_set_score(player: *mut IPlayer, score: i32) {
#[cfg(not(target_env = "msvc"))]
type SetScoreFn = unsafe extern "C" fn(*mut u8, i32);
#[cfg(target_env = "msvc")]
type SetScoreFn = unsafe extern "thiscall" fn(*mut u8, i32);
let Some((this, f_ptr)) = (unsafe {
super::vtable::secondary_call_target_ptr(player.cast::<u8>(), 0, SLOT_PLAYER_SET_SCORE)
}) else {
return;
};
let set_score: SetScoreFn = unsafe { std::mem::transmute(f_ptr) };
unsafe { set_score(this, score) };
}
#[must_use]
pub unsafe fn player_position(player: *mut IPlayer) -> Vector3 {
let zero = Vector3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
call_vtable!(
player.cast::<u8>(),
ENTITY_OFFSET,
SLOT_ENTITY_GET_POSITION,
() -> Vector3,
(),
zero
)
}
pub unsafe fn player_set_position(player: *mut IPlayer, position: Vector3) {
call_vtable!(
player.cast::<u8>(),
ENTITY_OFFSET,
SLOT_ENTITY_SET_POSITION,
(Vector3) -> (),
(position),
()
)
}
#[must_use]
pub unsafe fn player_virtual_world(player: *mut IPlayer) -> i32 {
call_vtable!(
player.cast::<u8>(),
ENTITY_OFFSET,
SLOT_ENTITY_GET_VIRTUAL_WORLD,
() -> i32,
(),
0
)
}
pub unsafe fn player_set_virtual_world(player: *mut IPlayer, world: i32) {
call_vtable!(
player.cast::<u8>(),
ENTITY_OFFSET,
SLOT_ENTITY_SET_VIRTUAL_WORLD,
(i32) -> (),
(world),
()
)
}
unsafe fn player_set_i32(player: *mut IPlayer, slot: usize, value: i32) {
call_vtable!(player.cast::<u8>(), 0, slot, (i32) -> (), (value), ())
}
unsafe fn player_get_i32(player: *mut IPlayer, slot: usize) -> i32 {
call_vtable!(player.cast::<u8>(), 0, slot, () -> i32, (), 0)
}
pub unsafe fn player_set_money(player: *mut IPlayer, amount: i32) {
unsafe { player_set_i32(player, SLOT_PLAYER_SET_MONEY, amount) };
}
pub unsafe fn player_give_money(player: *mut IPlayer, amount: i32) {
unsafe { player_set_i32(player, SLOT_PLAYER_GIVE_MONEY, amount) };
}
pub unsafe fn player_set_team(player: *mut IPlayer, team: i32) {
unsafe { player_set_i32(player, SLOT_PLAYER_SET_TEAM, team) };
}
#[must_use]
pub unsafe fn player_team(player: *mut IPlayer) -> i32 {
unsafe { player_get_i32(player, SLOT_PLAYER_GET_TEAM) }
}
pub unsafe fn player_set_armour(player: *mut IPlayer, armour: f32) {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_SET_ARMOUR, (f32) -> (), (armour), ())
}
#[must_use]
pub unsafe fn player_armour(player: *mut IPlayer) -> f32 {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_GET_ARMOUR, () -> f32, (), 0.0)
}
pub unsafe fn player_set_skin(player: *mut IPlayer, skin: i32, send: bool) {
call_vtable!(player.cast::<u8>(), 0, SLOT_PLAYER_SET_SKIN, (i32, bool) -> (), (skin, send), ())
}
#[must_use]
pub unsafe fn player_id(player: *mut IPlayer) -> i32 {
call_vtable!(player.cast::<u8>(), ENTITY_OFFSET, SLOT_ENTITY_GET_ID, () -> i32, (), -1)
}
#[must_use]
pub unsafe fn player_by_id(pool: *mut IPlayerPool, id: i32) -> *mut IPlayer {
call_vtable!(
pool.cast::<u8>(),
PLAYER_POOL_OFFSET,
SLOT_POOL_GET,
(i32) -> *mut IPlayer,
(id),
std::ptr::null_mut()
)
}
#[must_use]
pub unsafe fn player_money(player: *mut IPlayer) -> i32 {
unsafe { player_get_i32(player, SLOT_PLAYER_GET_MONEY) }
}
#[must_use]
pub unsafe fn player_skin(player: *mut IPlayer) -> i32 {
unsafe { player_get_i32(player, SLOT_PLAYER_GET_SKIN) }
}
pub unsafe fn player_set_wanted_level(player: *mut IPlayer, level: u32) {
unsafe { player_set_i32(player, SLOT_PLAYER_SET_WANTED, level as i32) };
}
#[must_use]
pub unsafe fn player_wanted_level(player: *mut IPlayer) -> u32 {
unsafe { player_get_i32(player, SLOT_PLAYER_GET_WANTED) as u32 }
}
pub unsafe fn player_set_interior(player: *mut IPlayer, interior: u32) {
unsafe { player_set_i32(player, SLOT_PLAYER_SET_INTERIOR, interior as i32) };
}
#[must_use]
pub unsafe fn player_interior(player: *mut IPlayer) -> u32 {
unsafe { player_get_i32(player, SLOT_PLAYER_GET_INTERIOR) as u32 }
}
pub unsafe fn player_set_weather(player: *mut IPlayer, weather: i32) {
unsafe { player_set_i32(player, SLOT_PLAYER_SET_WEATHER, weather) };
}
pub unsafe fn player_set_drunk_level(player: *mut IPlayer, level: i32) {
unsafe { player_set_i32(player, SLOT_PLAYER_SET_DRUNK, level) };
}
pub unsafe fn player_set_controllable(player: *mut IPlayer, controllable: bool) {
call_vtable!(
player.cast::<u8>(),
0,
SLOT_PLAYER_SET_CONTROLLABLE,
(bool) -> (),
(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 {
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 {
data: text.as_ptr(),
len: text.len(),
};
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 {
#[cfg(not(target_env = "msvc"))]
type AddFn = unsafe extern "C" fn(*mut u8, *mut $handler, i8) -> bool;
#[cfg(target_env = "msvc")]
type AddFn = unsafe extern "thiscall" fn(*mut u8, *mut $handler, i8) -> bool;
let Some((this, f_ptr)) = (unsafe {
super::vtable::secondary_call_target_ptr(dispatcher.cast::<u8>(), 0, 0)
}) else {
return false;
};
let add: AddFn = unsafe { std::mem::transmute(f_ptr) };
unsafe { add(this, handler, 0) }
}
};
}
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);
}
}