use super::*;
use crate::test_support::entities::{MockEntity, base_entity_fields, set_datamap, state_fields};
use crate::test_support::leak;
use crate::test_support::server::{mock_server, null_server};
use crate::test_support::tf2::script_binding::{
class_description, member_binding, set_script_description,
};
use crate::tf2::damage::CUSTOM_DAMAGE_PLASMA;
use sdk_raw::entities::EFL_KILLME;
use sdk_raw::test_support::entities::{data_map, field};
use sdk_raw::tf2::script_binding::{FLOAT, HANDLE, INT, QANGLE, VECTOR};
use std::cell::{Cell, RefCell};
use std::ffi::{CString, c_char, c_void};
type Call = (*mut c_void, CString, Vec<Argument>);
const INSTANCE_OFFSET: usize = 72;
const MEMBERS: [(&CStr, &[sys::ScriptDataType_t]); 14] = [
(c"AddSolidFlags", &[INT]),
(c"ApplyAbsVelocityImpulse", &[VECTOR]),
(c"ApplyLocalAngularVelocityImpulse", &[VECTOR]),
(c"RemoveSolidFlags", &[INT]),
(c"SetAbsAngles", &[QANGLE]),
(c"SetAbsOrigin", &[VECTOR]),
(c"SetAbsVelocity", &[VECTOR]),
(c"SetCollisionGroup", &[INT]),
(c"SetPhysAngularVelocity", &[VECTOR]),
(c"SetPhysVelocity", &[VECTOR]),
(c"SetSize", &[VECTOR, VECTOR]),
(c"SetSolid", &[INT]),
(c"SetSolidFlags", &[INT]),
(
c"TakeDamageCustom",
&[HANDLE, HANDLE, HANDLE, VECTOR, VECTOR, FLOAT, INT, INT],
),
];
const SCRIPT_ID_OFFSET: usize = 80;
#[derive(Debug, Clone, PartialEq)]
enum Argument {
Angles([f32; 3]),
Float(f32),
Handle(sys::HSCRIPT),
Int(c_int),
Vector([f32; 3]),
}
thread_local! {
static ACCEPTS: Cell<bool> = const { Cell::new(true) };
static CALLS: RefCell<Vec<Call>> = const { RefCell::new(Vec::new()) };
}
#[test]
fn damage_always_has_an_attacker_and_an_inflictor() {
let mut target = MockEntity::new(1);
let mut soldier = MockEntity::new(2);
let mut rocket = MockEntity::new(3);
let mut launcher = MockEntity::new(4);
let instances = [&mut target, &mut soldier, &mut rocket, &mut launcher].map(give_instance);
let [
target_instance,
soldier_instance,
rocket_instance,
launcher_instance,
] = instances;
let object = target.as_ptr().cast::<c_void>();
set_maps();
describe();
let scope = ();
let server = mock_server(&scope);
let soldier_entity = soldier.entity();
let rocket_entity = rocket.entity();
let launcher_entity = launcher.entity();
let entity = TfEntity::new(server, target.entity()).unwrap();
entity
.take_damage(Damage::new(25.0, DamageType::BULLET))
.unwrap();
entity
.take_damage(Damage::new(50.0, DamageType::CLUB).attacker(soldier_entity))
.unwrap();
entity
.take_damage(
Damage::new(90.0, DamageType::BLAST)
.inflictor(rocket_entity)
.weapon(launcher_entity)
.force(Vector::new(0.0, 0.0, 400.0))
.position(Vector::new(1.0, 2.0, 3.0))
.custom(CUSTOM_DAMAGE_PLASMA),
)
.unwrap();
entity
.take_damage(
Damage::new(-5.0, DamageType::empty())
.attacker(soldier_entity)
.inflictor(rocket_entity),
)
.unwrap();
let damage =
|inflictor, attacker, weapon, force, position, amount, kinds: DamageType, custom| {
(
object,
c"TakeDamageCustom".to_owned(),
vec![
Argument::Handle(inflictor),
Argument::Handle(attacker),
Argument::Handle(weapon),
Argument::Vector(force),
Argument::Vector(position),
Argument::Float(amount),
Argument::Int(kinds.bits() as c_int),
Argument::Int(custom),
],
)
};
let zero = [0.0; 3];
assert_eq!(
CALLS.take(),
[
damage(
target_instance,
target_instance,
null_mut(),
zero,
zero,
25.0,
DamageType::BULLET,
0,
),
damage(
soldier_instance,
soldier_instance,
null_mut(),
zero,
zero,
50.0,
DamageType::CLUB,
0,
),
damage(
rocket_instance,
rocket_instance,
launcher_instance,
[0.0, 0.0, 400.0],
[1.0, 2.0, 3.0],
90.0,
DamageType::BLAST,
CUSTOM_DAMAGE_PLASMA,
),
damage(
rocket_instance,
soldier_instance,
null_mut(),
zero,
zero,
-5.0,
DamageType::empty(),
0,
),
]
);
let nan = Vector::new(f32::NAN, 0.0, 0.0);
for damage in [
Damage::new(f32::INFINITY, DamageType::GENERIC),
Damage::new(1.0, DamageType::GENERIC).force(nan),
Damage::new(1.0, DamageType::GENERIC).position(nan),
] {
assert_eq!(entity.take_damage(damage), Err(TfEntityError::NonFinite));
}
assert!(CALLS.take().is_empty());
}
#[test]
fn damage_from_entities_marked_for_deletion_is_refused() {
let mut target = MockEntity::new(1);
let mut soldier = MockEntity::new(2);
give_instance(&mut target);
give_instance(&mut soldier);
soldier.set_eflags(EFL_KILLME);
set_maps();
describe();
let scope = ();
let server = mock_server(&scope);
let soldier = soldier.entity();
let entity = TfEntity::new(server, target.entity()).unwrap();
assert_eq!(
entity.take_damage(Damage::new(1.0, DamageType::GENERIC).attacker(soldier)),
Err(TfEntityError::ScriptInstance(
ScriptInstanceError::MarkedForDeletion
))
);
assert!(CALLS.take().is_empty());
}
fn describe() {
let bindings = MEMBERS
.iter()
.map(|&(name, parameters)| {
let parameters = parameters.to_vec().leak();
let mut binding = member_binding(name, binding::VOID, parameters, Some(record));
binding.m_pFunction.val_0 = name.as_ptr() as isize;
binding
})
.collect::<Vec<_>>();
set_script_description(leak(class_description(
c"CBaseEntity",
bindings.leak(),
null_mut(),
)));
ACCEPTS.set(true);
CALLS.take();
}
#[test]
fn entities_are_refused_unless_live_on_tf2_with_the_members() {
let mut mock = MockEntity::new(1);
set_maps();
describe();
let scope = ();
let server = mock_server(&scope);
assert!(matches!(
TfEntity::new(null_server(Game::SourceSdk2013, &scope), mock.entity()),
Err(TfEntityError::NotTf2)
));
ACCEPTS.set(false);
assert_eq!(
TfEntity::new(server, mock.entity())
.unwrap()
.set_collision_group(TfCollisionGroup::Rockets),
Err(TfEntityError::Rejected)
);
assert_eq!(take_names(), [c"SetCollisionGroup".to_owned()]);
set_script_description(null_mut());
assert_eq!(
TfEntity::new(server, mock.entity())
.unwrap()
.add_solid_flags(SolidFlags::NOT_SOLID),
Err(TfEntityError::UnsupportedMethod)
);
describe();
mock.set_eflags(EFL_KILLME);
let entity = TfEntity::new(server, mock.entity()).unwrap();
assert_eq!(
entity.set_solid_flags(SolidFlags::empty()),
Err(TfEntityError::MarkedForDeletion)
);
assert_eq!(
entity.take_damage(Damage::new(1.0, DamageType::GENERIC)),
Err(TfEntityError::MarkedForDeletion)
);
assert!(CALLS.take().is_empty());
}
fn give_instance(mock: &mut MockEntity) -> sys::HSCRIPT {
let instance = leak(0_u8).cast::<sys::HSCRIPT__>();
unsafe {
mock.as_ptr()
.byte_add(INSTANCE_OFFSET)
.cast::<sys::HSCRIPT>()
.write(instance);
}
instance
}
#[test]
fn members_receive_their_arguments() {
let mut mock = MockEntity::new(1);
let object = mock.as_ptr().cast::<c_void>();
set_maps();
describe();
let scope = ();
let server = mock_server(&scope);
let entity = TfEntity::new(server, mock.entity()).unwrap();
let flags = SolidFlags::NOT_SOLID | SolidFlags::TRIGGER;
assert_eq!(entity.entity().as_ptr(), object.cast());
entity.add_solid_flags(flags).unwrap();
entity.remove_solid_flags(SolidFlags::TRIGGER).unwrap();
entity.set_solid_flags(SolidFlags::empty()).unwrap();
entity.set_solid(SolidType::BoundingBox).unwrap();
entity
.set_collision_group(TfCollisionGroup::RespawnRooms)
.unwrap();
entity.set_abs_origin(Vector::new(4.0, 5.0, 6.0)).unwrap();
entity
.set_abs_angles(QAngle {
pitch: 10.0,
yaw: 20.0,
roll: 30.0,
})
.unwrap();
entity.set_abs_velocity(Vector::new(1.0, 2.0, 3.0)).unwrap();
entity.apply_impulse(Vector::new(0.0, 0.0, 300.0)).unwrap();
entity
.apply_angular_impulse(Vector::new(0.0, 90.0, 0.0))
.unwrap();
entity
.set_size(Vector::new(-8.0, -8.0, 0.0), Vector::new(8.0, 8.0, 16.0))
.unwrap();
entity
.set_size(Vector::new(1.0, 1.0, 1.0), Vector::new(1.0, 1.0, 1.0))
.unwrap();
let call = |name: &CStr, arguments| (object, name.to_owned(), arguments);
assert_eq!(
CALLS.take(),
[
call(
c"AddSolidFlags",
vec![Argument::Int(c_int::from(flags.bits()))]
),
call(
c"RemoveSolidFlags",
vec![Argument::Int(c_int::from(SolidFlags::TRIGGER.bits()))]
),
call(c"SetSolidFlags", vec![Argument::Int(0)]),
call(
c"SetSolid",
vec![Argument::Int(c_int::from(SolidType::BoundingBox.to_raw()))]
),
call(
c"SetCollisionGroup",
vec![Argument::Int(TfCollisionGroup::RespawnRooms.to_raw())]
),
call(c"SetAbsOrigin", vec![Argument::Vector([4.0, 5.0, 6.0])]),
call(c"SetAbsAngles", vec![Argument::Angles([10.0, 20.0, 30.0])]),
call(c"SetAbsVelocity", vec![Argument::Vector([1.0, 2.0, 3.0])]),
call(
c"ApplyAbsVelocityImpulse",
vec![Argument::Vector([0.0, 0.0, 300.0])]
),
call(
c"ApplyLocalAngularVelocityImpulse",
vec![Argument::Vector([0.0, 90.0, 0.0])]
),
call(
c"SetSize",
vec![
Argument::Vector([-8.0, -8.0, 0.0]),
Argument::Vector([8.0, 8.0, 16.0]),
]
),
call(
c"SetSize",
vec![
Argument::Vector([1.0, 1.0, 1.0]),
Argument::Vector([1.0, 1.0, 1.0]),
]
),
]
);
}
#[test]
fn physics_needs_a_physics_object() {
let mut mock = MockEntity::new(1);
let mut object = 0_u8;
set_maps();
describe();
let scope = ();
let server = mock_server(&scope);
let velocity = Vector::new(0.0, 0.0, 100.0);
{
let entity = TfEntity::new(server, mock.entity()).unwrap();
entity.apply_impulse(velocity).unwrap();
assert_eq!(
entity.set_physics_velocity(velocity),
Err(TfEntityError::NoPhysicsObject)
);
assert_eq!(
entity.set_physics_angular_velocity(velocity),
Err(TfEntityError::NoPhysicsObject)
);
}
mock.state().move_type = MoveType::VPhysics.to_raw();
{
let entity = TfEntity::new(server, mock.entity()).unwrap();
assert_eq!(
entity.apply_impulse(velocity),
Err(TfEntityError::NoPhysicsObject)
);
assert_eq!(
entity.apply_angular_impulse(velocity),
Err(TfEntityError::NoPhysicsObject)
);
}
assert_eq!(take_names(), [c"ApplyAbsVelocityImpulse".to_owned()]);
mock.state().physics_object = (&raw mut object).cast();
let entity = TfEntity::new(server, mock.entity()).unwrap();
entity.apply_impulse(velocity).unwrap();
entity.apply_angular_impulse(velocity).unwrap();
entity.set_physics_velocity(velocity).unwrap();
entity.set_physics_angular_velocity(velocity).unwrap();
assert_eq!(
take_names(),
[
c"ApplyAbsVelocityImpulse".to_owned(),
c"ApplyLocalAngularVelocityImpulse".to_owned(),
c"SetPhysVelocity".to_owned(),
c"SetPhysAngularVelocity".to_owned(),
]
);
}
unsafe extern "C" fn record(
function: sys::ScriptFunctionBindingStorageType_t,
object: *mut c_void,
arguments: *mut sys::ScriptVariant_t,
count: c_int,
result: *mut sys::ScriptVariant_t,
) -> bool {
assert!(result.is_null(), "every member returns nothing");
let (name, arguments) = unsafe {
(
CStr::from_ptr(function.val_0 as *const c_char),
std::slice::from_raw_parts(arguments, usize::try_from(count).unwrap()),
)
};
let arguments = arguments
.iter()
.map(|argument| {
let value = &argument.__bindgen_anon_1;
unsafe {
match c_int::from(argument.m_type) {
FLOAT => Argument::Float(value.m_float),
HANDLE => Argument::Handle(value.m_hScript),
INT => Argument::Int(value.m_int),
VECTOR => {
let vector = &*value.m_pVector;
Argument::Vector([vector.x, vector.y, vector.z])
}
QANGLE => {
let angles = &*value.m_pData.cast::<sys::QAngle>();
Argument::Angles([angles.x, angles.y, angles.z])
}
other => panic!("unexpected argument type {other}"),
}
}
})
.collect();
CALLS.with_borrow_mut(|calls| calls.push((object, name.to_owned(), arguments)));
ACCEPTS.get()
}
fn set_maps() {
let mut fields = Vec::from(base_entity_fields());
let mut script_id = field(
c"m_iszScriptId",
sys::_fieldtypes_FIELD_STRING,
SCRIPT_ID_OFFSET,
);
script_id.fieldSizeInBytes = size_of::<sys::string_t>() as c_int;
fields.extend(state_fields());
fields.push(script_id);
set_datamap(data_map(c"CBaseEntity", fields, null_mut()));
}
fn take_names() -> Vec<CString> {
CALLS.take().into_iter().map(|(_, name, _)| name).collect()
}
#[test]
fn values_the_game_mishandles_are_refused() {
let mut mock = MockEntity::new(1);
set_maps();
describe();
let scope = ();
let server = mock_server(&scope);
let entity = TfEntity::new(server, mock.entity()).unwrap();
let finite = Vector::new(1.0, 2.0, 3.0);
for value in [
Vector::new(f32::NAN, 0.0, 0.0),
Vector::new(0.0, f32::INFINITY, 0.0),
Vector::new(0.0, 0.0, f32::NEG_INFINITY),
] {
let angles = QAngle {
pitch: value.x,
yaw: value.y,
roll: value.z,
};
for result in [
entity.set_abs_origin(value),
entity.set_abs_angles(angles),
entity.set_abs_velocity(value),
entity.apply_impulse(value),
entity.apply_angular_impulse(value),
entity.set_physics_velocity(value),
entity.set_physics_angular_velocity(value),
entity.set_size(value, finite),
entity.set_size(Vector::new(-1.0, -2.0, -3.0), value),
] {
assert_eq!(result, Err(TfEntityError::NonFinite), "{value:?}");
}
}
for (mins, maxs) in [
(Vector::new(0.0, 0.0, 1.0), Vector::new(8.0, 8.0, 0.0)),
(Vector::new(9.0, -8.0, 0.0), Vector::new(8.0, 8.0, 8.0)),
] {
assert_eq!(
entity.set_size(mins, maxs),
Err(TfEntityError::InvalidBounds)
);
}
assert!(CALLS.take().is_empty());
}