use crate::entities::{Entity, EntityHandle, data_field_offset, data_map_class};
use crate::ffi::copy_cstr;
use std::ffi::{CString, c_int};
const MAX_PATH: usize = 260;
#[doc(alias = "CAmbientGeneric")]
#[doc(alias = "ambient_generic")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmbientSound<'s> {
entity: Entity<'s>,
layout: Layout,
}
impl<'s> AmbientSound<'s> {
pub fn new(entity: Entity<'s>) -> Option<Self> {
Some(Self {
entity,
layout: Layout::of(entity)?,
})
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
fn field<T>(self, offset: usize) -> *mut T {
unsafe { self.entity.as_ptr().byte_add(offset).cast() }
}
pub fn is_looping(self) -> bool {
unsafe { self.field::<u8>(self.layout.looping()).read() != 0 }
}
pub fn is_playing(self) -> bool {
unsafe { self.field::<u8>(self.layout.playing).read() != 0 }
}
pub fn set_source(self, source: Option<Entity<'_>>) {
let handle = source.map_or(EntityHandle::INVALID, Entity::handle);
unsafe {
self.field::<u32>(self.layout.source())
.write(handle.to_raw())
};
}
pub fn sound(self) -> Option<CString> {
unsafe {
copy_cstr(
self.field::<sys::string_t>(self.layout.sound)
.read()
.pszValue,
)
}
}
pub fn source(self) -> Option<EntityHandle> {
let handle =
EntityHandle::from_raw(unsafe { self.field::<u32>(self.layout.source()).read() });
handle.is_valid().then_some(handle)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Layout {
playing: usize,
source_name: usize,
sound: usize,
}
impl Layout {
fn of(entity: Entity<'_>) -> Option<Self> {
let map = entity
.data_maps()
.find(|&map| data_map_class(map) == Some(c"CAmbientGeneric"))?;
let offset = |name, field_type| data_field_offset(map, name, field_type);
let playing = offset(c"m_fActive", sys::_fieldtypes_FIELD_BOOLEAN)?;
let looping = offset(c"m_fLooping", sys::_fieldtypes_FIELD_BOOLEAN)?;
let source_name = offset(c"m_sSourceEntName", sys::_fieldtypes_FIELD_STRING)?;
let sound = offset(c"m_iszSound", sys::_fieldtypes_FIELD_SOUNDNAME)?;
let fits = looping == playing + 1
&& source_name == (looping + 1 + MAX_PATH).next_multiple_of(align_of::<sys::string_t>())
&& sound
== source_name
+ size_of::<sys::string_t>()
+ size_of::<sys::CBaseHandle>()
+ size_of::<c_int>()
&& sound < 1 << 16;
fits.then_some(Self {
playing,
source_name,
sound,
})
}
const fn looping(self) -> usize {
self.playing + 1
}
const fn source(self) -> usize {
self.source_name + size_of::<sys::string_t>()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::test_support::{
MockEntity, base_entity_fields, data_map, field, set_datamap,
};
use std::ffi::CStr;
const PLAYING: usize = 200;
const SOUND: usize = SOURCE_NAME + 16;
const SOURCE_NAME: usize = 464;
fn ambient_field(
name: &'static CStr,
field_type: sys::fieldtype_t,
offset: usize,
) -> sys::typedescription_t {
let mut field = field();
field.fieldType = field_type;
field.fieldName = name.as_ptr();
field.fieldOffset[0] = offset as c_int;
field
}
#[test]
fn fields_outside_the_datamap_are_read_and_written_in_place() {
let mut mock = MockEntity::new(1);
serve_ambient_map(SOUND);
let base = mock.as_ptr();
unsafe {
base.byte_add(PLAYING).cast::<u8>().write(1);
base.byte_add(PLAYING + 1).cast::<u8>().write(0);
base.byte_add(SOURCE_NAME + 8)
.cast::<u32>()
.write(0x0003_0011);
base.byte_add(SOUND)
.cast::<sys::string_t>()
.write(sys::string_t {
pszValue: c"Ambient.MachineHum".as_ptr(),
});
}
let sound = AmbientSound::new(mock.entity()).unwrap();
assert!(sound.is_playing());
assert!(!sound.is_looping());
assert_eq!(sound.sound().as_deref(), Some(c"Ambient.MachineHum"));
assert_eq!(sound.source(), Some(EntityHandle::from_raw(0x0003_0011)));
sound.set_source(None);
assert_eq!(
unsafe { base.byte_add(SOURCE_NAME + 8).cast::<u32>().read() },
u32::MAX
);
assert_eq!(sound.source(), None);
assert_eq!(
unsafe { base.byte_add(SOURCE_NAME + 12).cast::<c_int>().read() },
0
);
let mut other = MockEntity::new(0x0004_0022);
serve_ambient_map(SOUND);
sound.set_source(Some(other.entity()));
assert_eq!(sound.source(), Some(EntityHandle::from_raw(0x0004_0022)));
}
fn serve_ambient_map(sound: usize) {
let base = data_map(
c"CBaseEntity",
base_entity_fields().to_vec(),
std::ptr::null_mut(),
);
let ambient = data_map(
c"CAmbientGeneric",
vec![
ambient_field(c"m_iszSound", sys::_fieldtypes_FIELD_SOUNDNAME, sound),
ambient_field(c"m_radius", sys::_fieldtypes_FIELD_FLOAT, PLAYING - 120),
ambient_field(
c"m_sSourceEntName",
sys::_fieldtypes_FIELD_STRING,
SOURCE_NAME,
),
ambient_field(c"m_fActive", sys::_fieldtypes_FIELD_BOOLEAN, PLAYING),
ambient_field(c"m_fLooping", sys::_fieldtypes_FIELD_BOOLEAN, PLAYING + 1),
],
base,
);
set_datamap(ambient);
}
#[test]
fn the_file_name_pins_the_source_name() {
assert_eq!(
(PLAYING + 2 + MAX_PATH).next_multiple_of(align_of::<sys::string_t>()),
SOURCE_NAME
);
}
#[test]
fn unexpected_layouts_and_other_classes_are_refused() {
let mut mock = MockEntity::new(1);
assert!(AmbientSound::new(mock.entity()).is_none());
serve_ambient_map(SOUND + 8);
assert!(AmbientSound::new(mock.entity()).is_none());
}
}