use crate::datatables::NetPropError;
use crate::entities::{Entity, data_fields, data_map_class};
use crate::ffi::{borrow_cstr, copy_cstr};
use crate::interfaces::ServerTools;
use crate::server::{InterfaceError, Server};
use std::ffi::{CStr, CString, c_int};
use std::num::NonZero;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SoundscapeIndex(c_int);
impl SoundscapeIndex {
pub const NONE: Self = Self(-1);
pub const fn get(self) -> c_int {
self.0
}
pub const fn is_none(self) -> bool {
self.0 < 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SoundscapeId(NonZero<c_int>);
impl SoundscapeId {
pub const fn get(self) -> c_int {
self.0.get()
}
const fn new(id: c_int) -> Option<Self> {
match NonZero::new(id) {
Some(id) if id.get() > 0 => Some(Self(id)),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Layout {
name: usize,
disabled: usize,
}
impl Layout {
fn of(entity: Entity<'_>) -> Option<Self> {
let map = entity
.data_maps()
.find(|&map| data_map_class(map) == Some(c"CEnvSoundscape"))?;
let offset = |name: &CStr, field_type: sys::fieldtype_t| {
data_fields(map)
.iter()
.find(|field| {
field.fieldType == field_type
&& unsafe { borrow_cstr(field.fieldName) } == Some(name)
})
.and_then(|field| usize::try_from(field.fieldOffset[0]).ok())
};
let string = size_of::<sys::string_t>();
let name = offset(c"m_soundscapeName", sys::_fieldtypes_FIELD_STRING)?;
let positions = offset(c"m_positionNames[0]", sys::_fieldtypes_FIELD_STRING)?;
let proxy = offset(c"m_hProxySoundscape", sys::_fieldtypes_FIELD_EHANDLE)?;
let disabled = offset(c"m_bDisabled", sys::_fieldtypes_FIELD_BOOLEAN)?;
let fits = name.is_multiple_of(align_of::<sys::string_t>())
&& positions == name + string + 2 * size_of::<c_int>()
&& proxy == positions + 8 * string
&& disabled == proxy + size_of::<sys::CBaseHandle>()
&& disabled < 1 << 16;
fits.then_some(Self { name, disabled })
}
const fn index(self) -> usize {
self.name + size_of::<sys::string_t>()
}
const fn id(self) -> usize {
self.index() + size_of::<c_int>()
}
}
#[doc(alias = "CEnvSoundscape")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Soundscape<'s> {
entity: Entity<'s>,
layout: Layout,
}
impl<'s> Soundscape<'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 index(self) -> SoundscapeIndex {
SoundscapeIndex(unsafe { self.field::<c_int>(self.layout.index()).read() })
}
pub fn set_index(self, index: SoundscapeIndex) {
unsafe { self.field::<c_int>(self.layout.index()).write(index.0) };
}
pub fn id(self) -> Option<SoundscapeId> {
SoundscapeId::new(unsafe { self.field::<c_int>(self.layout.id()).read() })
}
pub fn name(self) -> Option<CString> {
unsafe {
copy_cstr(
self.field::<sys::string_t>(self.layout.name)
.read()
.pszValue,
)
}
}
pub fn is_enabled(self) -> bool {
unsafe { self.field::<u8>(self.layout.disabled).read() == 0 }
}
}
impl<'s> ServerTools<'s> {
pub fn spawn_soundscape(self, name: &CStr) -> Option<Soundscape<'s>> {
let entity = unsafe { self.create_entity_by_name(c"env_soundscape") }?;
self.set_key_value(entity, c"soundscape", name);
self.set_key_value(entity, c"StartDisabled", c"1");
unsafe { self.dispatch_spawn(entity) };
Soundscape::new(entity)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlayerSoundscape {
pub source: Option<SoundscapeId>,
pub index: SoundscapeIndex,
}
#[derive(Debug, thiserror::Error)]
pub enum SoundscapeError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error(transparent)]
NetProp(#[from] NetPropError),
}
pub fn player_soundscape<'s>(
server: Server<'s>,
player: Entity<'s>,
) -> Result<PlayerSoundscape, SoundscapeError> {
let game = server.server_game_dll()?;
let index = game
.entity_net_prop(player, c"m_audio.soundscapeIndex")?
.get::<i32>(player)?;
let source = game
.entity_net_prop(player, c"m_audio.entIndex")?
.get::<i32>(player)?;
Ok(PlayerSoundscape {
source: SoundscapeId::new(source),
index: SoundscapeIndex(index),
})
}
pub fn set_player_soundscape<'s>(
server: Server<'s>,
player: Entity<'s>,
soundscape: PlayerSoundscape,
) -> Result<(), SoundscapeError> {
let game = server.server_game_dll()?;
let engine = server.valve_engine()?;
let index = game.entity_net_prop(player, c"m_audio.soundscapeIndex")?;
let source = game.entity_net_prop(player, c"m_audio.entIndex")?;
let source_id = soundscape.source.map_or(0, SoundscapeId::get);
unsafe {
index.set(engine, player, soundscape.index.get())?;
source.set(engine, player, source_id)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::test_support::{
MockEntity, base_entity_fields, data_map, field, set_datamap,
};
const NAME: usize = 200;
fn soundscape_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
}
fn serve_soundscape_map(disabled: usize) {
let base = data_map(
c"CBaseEntity",
base_entity_fields().to_vec(),
std::ptr::null_mut(),
);
let soundscape = data_map(
c"CEnvSoundscape",
vec![
soundscape_field(c"m_flRadius", sys::_fieldtypes_FIELD_FLOAT, NAME - 8),
soundscape_field(c"m_soundscapeName", sys::_fieldtypes_FIELD_STRING, NAME),
soundscape_field(
c"m_hProxySoundscape",
sys::_fieldtypes_FIELD_EHANDLE,
NAME + 80,
),
soundscape_field(
c"m_positionNames[0]",
sys::_fieldtypes_FIELD_STRING,
NAME + 16,
),
soundscape_field(c"m_bDisabled", sys::_fieldtypes_FIELD_BOOLEAN, disabled),
],
base,
);
set_datamap(soundscape);
}
#[test]
fn fields_outside_the_datamap_are_read_and_written_in_place() {
let mut mock = MockEntity::new(1);
serve_soundscape_map(NAME + 84);
let base = mock.as_ptr();
unsafe {
base.byte_add(NAME + 8).cast::<c_int>().write(55);
base.byte_add(NAME + 12).cast::<c_int>().write(3);
base.byte_add(NAME)
.cast::<sys::string_t>()
.write(sys::string_t {
pszValue: c"Halloween.Outside".as_ptr(),
});
base.byte_add(NAME + 84).cast::<u8>().write(1);
}
let soundscape = Soundscape::new(mock.entity()).unwrap();
assert_eq!(soundscape.index(), SoundscapeIndex(55));
assert_eq!(soundscape.id().map(SoundscapeId::get), Some(3));
assert_eq!(soundscape.name().as_deref(), Some(c"Halloween.Outside"));
assert!(!soundscape.is_enabled());
soundscape.set_index(SoundscapeIndex::NONE);
assert_eq!(
unsafe { base.byte_add(NAME + 8).cast::<c_int>().read() },
-1
);
assert!(soundscape.index().is_none());
}
#[test]
fn unexpected_layouts_and_other_classes_are_refused() {
let mut mock = MockEntity::new(1);
assert!(Soundscape::new(mock.entity()).is_none());
serve_soundscape_map(NAME + 88);
assert!(Soundscape::new(mock.entity()).is_none());
}
#[test]
fn unassigned_ids_are_none() {
let mut mock = MockEntity::new(1);
serve_soundscape_map(NAME + 84);
unsafe { mock.as_ptr().byte_add(NAME + 12).cast::<c_int>().write(-1) };
assert_eq!(Soundscape::new(mock.entity()).unwrap().id(), None);
}
#[test]
fn soundscapes_cannot_be_removed() {
let mut mock = MockEntity::new(1);
serve_soundscape_map(NAME + 84);
assert!(mock.entity().is_protected());
}
}