use super::*;
use crate::test_support::entities::{
MOCK_EFLAGS_OFFSET, base_entity_fields, get_datamap, set_datamap,
};
use crate::test_support::server::mock_server;
use crate::test_support::tf2::script_binding::{
SCRIPT_DESCRIPTION_SLOT, class_description, member_binding, script_description,
set_script_description,
};
use sdk_raw::players::{LIFE_DEAD, LIFE_DYING};
use sdk_raw::test_support::entities::{data_map, field};
use sdk_raw::tf2::script_binding::STRING;
use std::cell::{Cell, RefCell};
use std::ffi::{c_char, c_int, c_void};
use std::mem::offset_of;
use std::ptr::NonNull;
const PLAY_SCENE: usize =
sdk_raw::vtable_slot!(sys::CTFPlayer__bindgen_vtable, CTFPlayer_PlayScene);
thread_local! {
static LENGTH: Cell<f32> = const { Cell::new(0.0) };
static PLAYED: RefCell<Vec<(*mut c_void, CString, f32)>> = const { RefCell::new(Vec::new()) };
static REJECT: Cell<bool> = const { Cell::new(false) };
}
#[repr(C)]
struct FakePlayer {
vtable: *const *const (),
padding: [usize; 3],
eflags: c_int,
more_padding: [u8; 28],
life_state: u8,
}
impl FakePlayer {
fn new(vtable: &[*const ()]) -> Box<Self> {
Box::new(Self {
vtable: vtable.as_ptr(),
padding: [0; 3],
eflags: 0,
more_padding: [0; 28],
life_state: LIFE_ALIVE,
})
}
}
fn life_state_field() -> sys::typedescription_t {
let mut life_state = field(
c"m_lifeState",
sys::_fieldtypes_FIELD_CHARACTER,
offset_of!(FakePlayer, life_state),
);
life_state.fieldSize = 1;
life_state.fieldSizeInBytes = 1;
life_state
}
#[test]
fn living_tf_players_speak_scenes_through_the_generated_vtable() {
assert_eq!(offset_of!(FakePlayer, eflags), MOCK_EFLAGS_OFFSET);
let mut vtable = vec![std::ptr::null(); PLAY_SCENE + 1];
vtable[sdk_raw::entities::GET_DATA_DESC_MAP_SLOT] = get_datamap as *const ();
vtable[PLAY_SCENE] = play_scene as *const ();
let player = Box::into_raw(FakePlayer::new(&vtable));
let entity = unsafe { Entity::from_raw(NonNull::new(player.cast()).unwrap()) };
let scope = ();
let server = mock_server(&scope);
set_datamap(maps(c"CTFPlayer", Some(life_state_field())));
PLAYED.take();
let speaker = Speaker::new(server, entity).unwrap();
let thanks = ScenePath::voice_line(PlayerClass::Scout, 508).unwrap();
assert!(speaker.is_alive());
LENGTH.set(0.8156);
assert_eq!(
speaker.play_scene(&thanks),
Ok(Duration::from_secs_f32(0.8156))
);
assert_eq!(
PLAYED.take(),
[(
player.cast(),
c"scenes/Player/Scout/low/508.vcd".to_owned(),
0.0
)]
);
for length in [0.0, -1.0, f32::NAN, f32::INFINITY] {
LENGTH.set(length);
assert_eq!(speaker.play_scene(&thanks), Err(VoiceError::MissingScene));
}
PLAYED.take();
unsafe { (&raw mut (*player).life_state).write(LIFE_DEAD) };
assert!(!speaker.is_alive());
assert_eq!(speaker.play_scene(&thanks), Err(VoiceError::NotAlive));
unsafe {
(&raw mut (*player).life_state).write(LIFE_ALIVE);
(&raw mut (*player).eflags).write(1);
}
assert_eq!(
speaker.play_scene(&thanks),
Err(VoiceError::MarkedForDeletion)
);
assert!(PLAYED.take().is_empty());
unsafe { drop(Box::from_raw(player)) };
}
fn maps(class: &'static CStr, life_state: Option<sys::typedescription_t>) -> *mut sys::datamap_t {
let mut fields = Vec::from(base_entity_fields());
fields.extend(life_state);
let base = data_map(c"CBaseEntity", fields, null_mut());
data_map(class, vec![], base)
}
unsafe extern "C" fn play_scene(
player: *mut sys::CTFPlayer,
scene: *const c_char,
delay: f32,
response: *mut sys::AI_Response,
filter: *mut sys::IRecipientFilter,
) -> f32 {
assert!(response.is_null());
assert!(
filter.is_null(),
"a filter would be cast to CRecipientFilter"
);
let scene = unsafe { CStr::from_ptr(scene) }.to_owned();
PLAYED.with_borrow_mut(|played| played.push((player.cast(), scene, delay)));
LENGTH.get()
}
unsafe extern "C" fn play_scene_adapter(
_: sys::ScriptFunctionBindingStorageType_t,
object: *mut c_void,
arguments: *mut sys::ScriptVariant_t,
count: c_int,
result: *mut sys::ScriptVariant_t,
) -> bool {
assert_eq!(count, 2);
if REJECT.get() {
return false;
}
let (scene, delay) = unsafe {
(
CStr::from_ptr((*arguments).__bindgen_anon_1.m_pszString).to_owned(),
(*arguments.add(1)).__bindgen_anon_1.m_float,
)
};
PLAYED.with_borrow_mut(|played| played.push((object, scene, delay)));
unsafe { result.write(binding::float(LENGTH.get())) };
true
}
#[test]
fn scripted_scenes_use_the_checked_cbaseflex_member() {
let mut parameters = [STRING, binding::FLOAT];
let mut bindings = [member_binding(
c"PlayScene",
binding::FLOAT,
&mut parameters,
Some(play_scene_adapter),
)];
let mut flex = class_description(c"CBaseFlex", &mut bindings, null_mut());
let binding = flex.m_FunctionBindings.m_Memory.m_pMemory;
let mut player_description = class_description(c"CTFPlayer", &mut [], &raw mut flex);
let mut vtable = vec![std::ptr::null(); PLAY_SCENE + 1];
vtable[sdk_raw::entities::GET_DATA_DESC_MAP_SLOT] = get_datamap as *const ();
vtable[SCRIPT_DESCRIPTION_SLOT] = script_description as *const ();
vtable[PLAY_SCENE] = sdk_raw::test_support::unexpected_call as *const ();
let player = Box::into_raw(FakePlayer::new(&vtable));
let entity = unsafe { Entity::from_raw(NonNull::new(player.cast()).unwrap()) };
let scope = ();
let server = mock_server(&scope);
set_datamap(maps(c"CTFPlayer", Some(life_state_field())));
set_script_description(&raw mut player_description);
PLAYED.take();
let speaker = Speaker::new(server, entity).unwrap();
let line = ScenePath::voice_line(PlayerClass::Heavy, "cm_heavy_gamewon_01").unwrap();
LENGTH.set(2.5);
assert_eq!(
speaker.play_scene_scripted(&line),
Ok(Duration::from_secs_f32(2.5))
);
assert_eq!(
PLAYED.take(),
[(
player.cast(),
c"scenes/Player/Heavy/low/cm_heavy_gamewon_01.vcd".to_owned(),
0.0
)]
);
LENGTH.set(0.0);
assert_eq!(
speaker.play_scene_scripted(&line),
Err(VoiceError::MissingScene)
);
REJECT.set(true);
assert_eq!(
speaker.play_scene_scripted(&line),
Err(VoiceError::Rejected)
);
REJECT.set(false);
unsafe { (&raw mut (*player).life_state).write(LIFE_DYING) };
assert_eq!(
speaker.play_scene_scripted(&line),
Err(VoiceError::NotAlive)
);
unsafe { (&raw mut (*player).life_state).write(LIFE_ALIVE) };
unsafe { (*binding).m_desc.m_ReturnType = binding::VOID };
assert_eq!(
speaker.play_scene_scripted(&line),
Err(VoiceError::UnsupportedMethod)
);
set_script_description(null_mut());
assert_eq!(
speaker.play_scene_scripted(&line),
Err(VoiceError::UnsupportedMethod)
);
PLAYED.take();
unsafe { drop(Box::from_raw(player)) };
}
#[test]
fn speakers_require_tf_players_with_a_life_state() {
let mut vtable = vec![std::ptr::null(); PLAY_SCENE + 1];
vtable[sdk_raw::entities::GET_DATA_DESC_MAP_SLOT] = get_datamap as *const ();
let player = Box::into_raw(FakePlayer::new(&vtable));
let entity = unsafe { Entity::from_raw(NonNull::new(player.cast()).unwrap()) };
let scope = ();
let server = mock_server(&scope);
set_datamap(maps(c"CTFPlayer", Some(life_state_field())));
assert!(Speaker::new(server, entity).is_ok());
set_datamap(data_map(
c"CTFBot",
vec![],
maps(c"CTFPlayer", Some(life_state_field())),
));
assert!(Speaker::new(server, entity).is_ok());
set_datamap(maps(c"CBaseCombatCharacter", Some(life_state_field())));
assert!(matches!(
Speaker::new(server, entity),
Err(VoiceError::NotTfPlayer)
));
set_datamap(maps(c"CTFPlayer", None));
assert!(matches!(
Speaker::new(server, entity),
Err(VoiceError::UnsupportedLayout)
));
let mut wide = life_state_field();
wide.fieldSizeInBytes = 4;
let mut array = life_state_field();
array.fieldSize = 2;
let mut integer = life_state_field();
integer.fieldType = sys::_fieldtypes_FIELD_INTEGER;
let mut far = life_state_field();
far.fieldOffset[0] = BASE_ENTITY_FIELD_OFFSET_LIMIT as c_int;
for life_state in [wide, array, integer, far] {
set_datamap(maps(c"CTFPlayer", Some(life_state)));
assert!(matches!(
Speaker::new(server, entity),
Err(VoiceError::UnsupportedLayout)
));
}
unsafe { drop(Box::from_raw(player)) };
}