Implement land, terrain, environment, and sound managers (#70)
All checks were successful
Native code generation / deterministic (push) Successful in 15m1s
Imaging and meshing gate / native (push) Successful in 5m22s
Native Rust workspace compile / compile (push) Successful in 5m10s

This commit is contained in:
2026-08-10 16:53:35 +00:00
parent 13f85bfa34
commit d537a37172
14 changed files with 4890 additions and 2028 deletions

View File

@@ -0,0 +1,497 @@
//! Sound trigger routing and simulator sound event decoding.
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::collapsible_if)] // Packet decoding and payload bounds remain distinct checks.
#![allow(clippy::unnecessary_wraps)] // Mapped constructors have fixed fallible signatures.
use crate::agent_manager::EventRegistry;
use crate::packet_catalog::{GeneratedPacket, PacketType};
use crate::{Error, GridClient, Simulator};
use libremetaverse_types::compat::{EventHandler, Subscription};
use libremetaverse_types::{SoundFlags, UUID, Vector3};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
const MAX_SOUND_PACKET_BYTES: usize = 1024 * 1024;
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
value
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[derive(Clone)]
pub struct AttachedSoundEventArgs {
sim: Simulator,
sound_id: UUID,
owner_id: UUID,
object_id: UUID,
gain: f32,
flags: SoundFlags,
}
impl AttachedSoundEventArgs {
pub fn new(
sim: Simulator,
sound_id: UUID,
owner_id: UUID,
object_id: UUID,
gain: f32,
flags: SoundFlags,
) -> Result<Self, Error> {
Ok(Self {
sim,
sound_id,
owner_id,
object_id,
gain,
flags,
})
}
pub fn simulator(&self) -> Simulator {
self.sim.clone()
}
pub fn sound_id(&self) -> UUID {
self.sound_id
}
pub fn owner_id(&self) -> UUID {
self.owner_id
}
pub fn object_id(&self) -> UUID {
self.object_id
}
pub fn gain(&self) -> f32 {
self.gain
}
pub fn flags(&self) -> SoundFlags {
self.flags
}
}
#[derive(Clone)]
pub struct AttachedSoundGainChangeEventArgs {
sim: Simulator,
object_id: UUID,
gain: f32,
}
impl AttachedSoundGainChangeEventArgs {
pub fn new(sim: Simulator, object_id: UUID, gain: f32) -> Result<Self, Error> {
Ok(Self {
sim,
object_id,
gain,
})
}
pub fn simulator(&self) -> Simulator {
self.sim.clone()
}
pub fn object_id(&self) -> UUID {
self.object_id
}
pub fn gain(&self) -> f32 {
self.gain
}
}
#[derive(Clone)]
pub struct PreloadSoundEventArgs {
sim: Simulator,
sound_id: UUID,
owner_id: UUID,
object_id: UUID,
}
impl PreloadSoundEventArgs {
pub fn new(
sim: Simulator,
sound_id: UUID,
owner_id: UUID,
object_id: UUID,
) -> Result<Self, Error> {
Ok(Self {
sim,
sound_id,
owner_id,
object_id,
})
}
pub fn simulator(&self) -> Simulator {
self.sim.clone()
}
pub fn sound_id(&self) -> UUID {
self.sound_id
}
pub fn owner_id(&self) -> UUID {
self.owner_id
}
pub fn object_id(&self) -> UUID {
self.object_id
}
}
#[derive(Clone)]
pub struct SoundTriggerEventArgs {
sim: Simulator,
sound_id: UUID,
owner_id: UUID,
object_id: UUID,
parent_id: UUID,
gain: f32,
region_handle: u64,
position: Vector3,
}
impl SoundTriggerEventArgs {
pub fn new(
sim: Simulator,
sound_id: UUID,
owner_id: UUID,
object_id: UUID,
parent_id: UUID,
gain: f32,
region_handle: u64,
position: Vector3,
) -> Result<Self, Error> {
Ok(Self {
sim,
sound_id,
owner_id,
object_id,
parent_id,
gain,
region_handle,
position,
})
}
pub fn simulator(&self) -> Simulator {
self.sim.clone()
}
pub fn sound_id(&self) -> UUID {
self.sound_id
}
pub fn owner_id(&self) -> UUID {
self.owner_id
}
pub fn object_id(&self) -> UUID {
self.object_id
}
pub fn parent_id(&self) -> UUID {
self.parent_id
}
pub fn gain(&self) -> f32 {
self.gain
}
pub fn region_handle(&self) -> u64 {
self.region_handle
}
pub fn position(&self) -> Vector3 {
self.position
}
}
#[derive(Default)]
struct SoundEvents {
attached: EventRegistry<AttachedSoundEventArgs>,
gain: EventRegistry<AttachedSoundGainChangeEventArgs>,
preload: EventRegistry<PreloadSoundEventArgs>,
trigger: EventRegistry<SoundTriggerEventArgs>,
}
pub(crate) struct SoundManagerInner {
client: Arc<GridClient>,
events: SoundEvents,
raw_subscription: Mutex<Option<Subscription>>,
disposed: AtomicBool,
}
#[derive(Clone)]
pub struct SoundManager {
pub(crate) inner: Arc<SoundManagerInner>,
}
impl SoundManager {
pub fn new(client: GridClient) -> Result<Self, Error> {
Self::native_new(Arc::new(client))
}
pub(crate) fn native_new(client: Arc<GridClient>) -> Result<Self, Error> {
if let Some(inner) = client.cached_sound_manager_inner() {
return Ok(Self { inner });
}
let inner = Arc::new(SoundManagerInner {
client: Arc::clone(&client),
events: SoundEvents::default(),
raw_subscription: Mutex::new(None),
disposed: AtomicBool::new(false),
});
let weak = Arc::downgrade(&inner);
*mutex(&inner.raw_subscription) = Some(client.network().subscribe_raw_packet(Arc::new(
move |event| {
if let Some(inner) = weak.upgrade() {
inner.handle(event);
}
},
)));
Ok(Self {
inner: client.install_sound_manager_inner(inner),
})
}
pub fn subscribe_attached_sound(
&self,
handler: EventHandler<AttachedSoundEventArgs>,
) -> Subscription {
self.inner.events.attached.subscribe(handler)
}
pub fn subscribe_attached_sound_gain_change(
&self,
handler: EventHandler<AttachedSoundGainChangeEventArgs>,
) -> Subscription {
self.inner.events.gain.subscribe(handler)
}
pub fn subscribe_preload_sound(
&self,
handler: EventHandler<PreloadSoundEventArgs>,
) -> Subscription {
self.inner.events.preload.subscribe(handler)
}
pub fn subscribe_sound_trigger(
&self,
handler: EventHandler<SoundTriggerEventArgs>,
) -> Subscription {
self.inner.events.trigger.subscribe(handler)
}
pub fn dispose(&self) -> Result<(), Error> {
self.inner.disposed.store(true, Ordering::Release);
mutex(&self.inner.raw_subscription).take();
Ok(())
}
pub fn play_sound(&self, sound_id: UUID) -> Result<(), Error> {
let mut client = (*self.inner.client).clone();
let position = client.self_().sim_position();
self.send_sound_trigger_with_uuid_vector3_single(sound_id, position, 1.0)
}
pub fn send_sound_trigger_with_uuid_vector3(
&self,
sound_id: UUID,
position: Vector3,
) -> Result<(), Error> {
self.send_sound_trigger_with_uuid_vector3_single(sound_id, position, 1.0)
}
pub fn send_sound_trigger_with_uuid_vector3_single(
&self,
sound_id: UUID,
position: Vector3,
gain: f32,
) -> Result<(), Error> {
let sim = self
.inner
.client
.network()
.current_sim()
.ok_or(Error::InvalidOperation)?;
self.send_to_sim(sound_id, &sim, sim.handle, position, gain)
}
pub fn send_sound_trigger_with_uuid_simulator_vector3_single(
&self,
sound_id: UUID,
sim: Simulator,
position: Vector3,
gain: f32,
) -> Result<(), Error> {
self.send_to_sim(sound_id, &sim, sim.handle, position, gain)
}
pub fn send_sound_trigger_with_uuid_u_int64_vector3_single(
&self,
sound_id: UUID,
handle: u64,
position: Vector3,
gain: f32,
) -> Result<(), Error> {
let sim = self
.inner
.client
.network()
.current_sim()
.ok_or(Error::InvalidOperation)?;
self.send_to_sim(sound_id, &sim, handle, position, gain)
}
fn send_to_sim(
&self,
sound_id: UUID,
sim: &Simulator,
handle: u64,
position: Vector3,
gain: f32,
) -> Result<(), Error> {
if self.inner.disposed.load(Ordering::Acquire) {
return Err(Error::InvalidOperation);
}
if sound_id == UUID::zero()
|| !gain.is_finite()
|| !position.x.is_finite()
|| !position.y.is_finite()
|| !position.z.is_finite()
{
return Err(Error::Argument);
}
let mut packet = crate::packets::SoundTriggerPacket::new_with_constructor()?;
packet.sound_data.sound_id = sound_id;
packet.sound_data.object_id = UUID::zero();
packet.sound_data.owner_id = UUID::zero();
packet.sound_data.parent_id = UUID::zero();
packet.sound_data.handle = handle;
packet.sound_data.position = position;
packet.sound_data.gain = gain.clamp(0.0, 1.0);
let bytes = packet.to_bytes_with_method()?;
sim.native_send_packet_data(
bytes.clone(),
i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
PacketType::SoundTrigger,
false,
)
}
}
impl SoundManagerInner {
fn handle(&self, event: crate::network_manager::RawPacketReceivedEventArgs) {
if self.disposed.load(Ordering::Acquire) || event.data.len() > MAX_SOUND_PACKET_BYTES {
return;
}
let mut position = 0;
match event.packet_type {
PacketType::AttachedSound => {
if let Ok(packet) =
crate::packets::AttachedSoundPacket::new_from_bytes(&event.data, &mut position)
{
let value = packet.data_block;
self.events.attached.emit(AttachedSoundEventArgs {
sim: event.simulator,
sound_id: value.sound_id,
owner_id: value.owner_id,
object_id: value.object_id,
gain: value.gain,
flags: SoundFlags(value.flags),
});
}
}
PacketType::AttachedSoundGainChange => {
if let Ok(packet) = crate::packets::AttachedSoundGainChangePacket::new_from_bytes(
&event.data,
&mut position,
) {
self.events.gain.emit(AttachedSoundGainChangeEventArgs {
sim: event.simulator,
object_id: packet.data_block.object_id,
gain: packet.data_block.gain,
});
}
}
PacketType::PreloadSound => {
if let Ok(packet) =
crate::packets::PreloadSoundPacket::new_from_bytes(&event.data, &mut position)
{
if packet.data_block.len() <= 65_535 {
for value in packet.data_block {
self.events.preload.emit(PreloadSoundEventArgs {
sim: event.simulator.clone(),
sound_id: value.sound_id,
owner_id: value.owner_id,
object_id: value.object_id,
});
}
}
}
}
PacketType::SoundTrigger => {
if let Ok(packet) =
crate::packets::SoundTriggerPacket::new_from_bytes(&event.data, &mut position)
{
let value = packet.sound_data;
self.events.trigger.emit(SoundTriggerEventArgs {
sim: event.simulator,
sound_id: value.sound_id,
owner_id: value.owner_id,
object_id: value.object_id,
parent_id: value.parent_id,
gain: value.gain,
region_handle: value.handle,
position: value.position,
});
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sound_trigger_rejects_zero_ids_and_non_finite_gain() {
let client = GridClient::new().unwrap();
let manager = SoundManager::new(client).unwrap();
assert!(
manager
.send_sound_trigger_with_uuid_u_int64_vector3_single(
UUID::zero(),
0,
Vector3::default(),
1.0
)
.is_err()
);
}
#[test]
fn event_args_preserve_gain_and_queue_identity() {
let client = GridClient::new().unwrap();
let sim = Simulator::new(client, "127.0.0.1:13".parse().unwrap(), 0, None, None).unwrap();
let id = UUID::random().unwrap();
let value =
AttachedSoundEventArgs::new(sim, id, UUID::zero(), UUID::zero(), 0.25, SoundFlags(2))
.unwrap();
assert_eq!(value.sound_id(), id);
assert_eq!(value.gain(), 0.25);
assert_eq!(value.flags(), SoundFlags(2));
}
#[test]
fn attached_sound_packet_preserves_ids_gain_and_queue_flag() {
let client = GridClient::new().unwrap();
let sim = Simulator::new(
client.clone(),
"127.0.0.1:13".parse().unwrap(),
0,
None,
None,
)
.unwrap();
let manager = SoundManager::new(client).unwrap();
let observed = Arc::new(Mutex::new(None));
let captured = Arc::clone(&observed);
let _subscription = manager.subscribe_attached_sound(Arc::new(move |event| {
*mutex(&captured) = Some((
event.sound_id(),
event.owner_id(),
event.object_id(),
event.gain(),
event.flags(),
));
}));
let sound_id = UUID::random().unwrap();
let owner_id = UUID::random().unwrap();
let object_id = UUID::random().unwrap();
let mut packet = crate::packets::AttachedSoundPacket::new_with_constructor().unwrap();
packet.data_block.sound_id = sound_id;
packet.data_block.owner_id = owner_id;
packet.data_block.object_id = object_id;
packet.data_block.gain = 0.375;
packet.data_block.flags = 2;
manager
.inner
.handle(crate::network_manager::RawPacketReceivedEventArgs {
packet_type: PacketType::AttachedSound,
data: packet.to_bytes_with_method().unwrap(),
simulator: sim,
});
assert_eq!(
mutex(&observed).clone(),
Some((sound_id, owner_id, object_id, 0.375, SoundFlags(2)))
);
}
}