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

@@ -403,3 +403,13 @@ library copy and transactional COF application with monotonic progress,
cancellation, and compensation. Exact mappings, hashes, generated visual data,
pixels, packet/event payloads, and recovery behavior are covered by focused
tests and documented in [`docs/appearance.md`](docs/appearance.md).
Native land services now correlate parcel overlays, properties, access lists,
owners, dwell, remote lookups, and bounded script-resource capabilities while
maintaining the simulator's four-metre parcel map. The 16×16 terrain codec
handles normal and rectangular large-region patches, wind layers, and four-slot
PBR material overrides. Extended and legacy environment LLSD paths preserve
parcel-versus-region URI rules, and sound packets retain identity, gain,
position, and queue flags. Packet/capability selection, state-before-event
ordering, cancellation, limits, and live-operation safety are documented in
[`docs/land.md`](docs/land.md).

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status |
|---|---:|---:|---|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 253 types / 15,906 members; remaining surface is callable failure-only shims |
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 290 types / 16,219 members; remaining surface is callable failure-only shims |
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -119,6 +119,10 @@ struct ClientRuntime {
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
object_manager: Mutex<Option<Arc<crate::object_manager::ObjectManagerInner>>>,
environment_manager: Mutex<Option<Arc<crate::environment_manager::EnvironmentManagerInner>>>,
terrain_manager: Mutex<Option<Arc<crate::terrain_manager::TerrainManagerInner>>>,
sound_manager: Mutex<Option<Arc<crate::sound_manager::SoundManagerInner>>>,
parcel_manager: Mutex<Option<Arc<crate::parcel_manager::ParcelManagerInner>>>,
appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
@@ -154,6 +158,10 @@ impl ClientRuntime {
network_manager: Mutex::new(std::sync::Weak::new()),
agent_manager: Mutex::new(std::sync::Weak::new()),
object_manager: Mutex::new(None),
environment_manager: Mutex::new(None),
terrain_manager: Mutex::new(None),
sound_manager: Mutex::new(None),
parcel_manager: Mutex::new(None),
appearance_manager: Mutex::new(None),
avatar_manager: Mutex::new(None),
animesh_manager: Mutex::new(None),
@@ -684,6 +692,159 @@ impl GridClient {
self.cache_object_manager_inner(&value.inner);
}
pub(crate) fn install_environment_manager_inner(
&self,
candidate: Arc<crate::environment_manager::EnvironmentManagerInner>,
) -> Arc<crate::environment_manager::EnvironmentManagerInner> {
let mut cached = self
.runtime
.environment_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_environment(&self) -> Result<crate::EnvironmentManager, crate::Error> {
if let Some(inner) = self
.runtime
.environment_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
{
return Ok(crate::EnvironmentManager { inner });
}
crate::environment_manager::EnvironmentManager::native_new(Arc::new(self.clone()))
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_environment(&self, value: crate::EnvironmentManager) {
*self
.runtime
.environment_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_terrain_manager_inner(
&self,
) -> Option<Arc<crate::terrain_manager::TerrainManagerInner>> {
self.runtime
.terrain_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_terrain_manager_inner(
&self,
candidate: Arc<crate::terrain_manager::TerrainManagerInner>,
) -> Arc<crate::terrain_manager::TerrainManagerInner> {
let mut cached = self
.runtime
.terrain_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_terrain(&self) -> Result<crate::TerrainManager, crate::Error> {
if let Some(inner) = self.cached_terrain_manager_inner() {
Ok(crate::TerrainManager { inner })
} else {
crate::terrain_manager::TerrainManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_terrain(&self, value: crate::TerrainManager) {
*self
.runtime
.terrain_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_sound_manager_inner(
&self,
) -> Option<Arc<crate::sound_manager::SoundManagerInner>> {
self.runtime
.sound_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_sound_manager_inner(
&self,
candidate: Arc<crate::sound_manager::SoundManagerInner>,
) -> Arc<crate::sound_manager::SoundManagerInner> {
let mut cached = self
.runtime
.sound_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_sound(&self) -> Result<crate::SoundManager, crate::Error> {
if let Some(inner) = self.cached_sound_manager_inner() {
Ok(crate::SoundManager { inner })
} else {
crate::sound_manager::SoundManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_sound(&self, value: crate::SoundManager) {
*self
.runtime
.sound_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_parcel_manager_inner(
&self,
) -> Option<Arc<crate::parcel_manager::ParcelManagerInner>> {
self.runtime
.parcel_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_parcel_manager_inner(
&self,
candidate: Arc<crate::parcel_manager::ParcelManagerInner>,
) -> Arc<crate::parcel_manager::ParcelManagerInner> {
let mut cached = self
.runtime
.parcel_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_parcels(&self) -> Result<crate::ParcelManager, crate::Error> {
if let Some(inner) = self.cached_parcel_manager_inner() {
Ok(crate::ParcelManager { inner })
} else {
crate::parcel_manager::ParcelManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_parcels(&self, value: crate::ParcelManager) {
*self
.runtime
.parcel_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn native_set_caps_rate_limiter(
&mut self,
value: crate::caps_http::CapsRateLimiter,

View File

@@ -0,0 +1,651 @@
//! Extended and legacy environment capability support.
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
use crate::agent_manager::EventRegistry;
use crate::{Error, GridClient};
use futures_channel::oneshot;
use futures_util::future::{Either, select};
use futures_util::pin_mut;
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
const MAX_ENVIRONMENT_BYTES: usize = 16 * 1024 * 1024;
fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
value
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
value
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn integer(map: &OSDMap, key: &str, default: i32) -> i32 {
map.get(key)
.and_then(|value| value.as_integer().ok())
.unwrap_or(default)
}
fn boolean(map: &OSDMap, key: &str, default: bool) -> bool {
map.get(key)
.and_then(|value| value.as_boolean().ok())
.unwrap_or(default)
}
fn string(map: &OSDMap, key: &str) -> String {
map.get(key)
.and_then(|value| value.as_string().ok())
.unwrap_or_default()
}
#[derive(Clone, Debug, PartialEq)]
pub struct EnvironmentData {
day_cycle: Option<OSD>,
day_length: i32,
day_offset: i32,
flags: crate::messages::linden::EnvironmentFlags,
is_default: bool,
}
impl EnvironmentData {
pub fn new() -> Result<Self, Error> {
Ok(Self {
day_cycle: None,
day_length: 14_400,
day_offset: 57_600,
flags: crate::messages::linden::EnvironmentFlags::NONE,
is_default: false,
})
}
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
self.day_length = integer(&map, "day_length", 14_400);
self.day_offset = integer(&map, "day_offset", 57_600);
self.flags = crate::messages::linden::EnvironmentFlags(
u32::try_from(integer(&map, "flags", 0)).unwrap_or_default(),
);
self.is_default = boolean(&map, "is_default", false);
self.day_cycle = map
.get("day_cycle")
.filter(|value| !matches!(value, OSD::Undefined));
Ok(())
}
pub fn serialize(&self) -> Result<OSDMap, Error> {
let mut values = HashMap::from([
("day_length".into(), OSD::Integer(self.day_length)),
("day_offset".into(), OSD::Integer(self.day_offset)),
(
"flags".into(),
OSD::Integer(i32::try_from(self.flags.0).unwrap_or(i32::MAX)),
),
]);
if let Some(day_cycle) = self.day_cycle.clone() {
values.insert("day_cycle".into(), day_cycle);
}
OSDMap::new_with_dictionary(values)
}
pub fn day_cycle(&self) -> Option<OSD> {
self.day_cycle.clone()
}
pub fn set_day_cycle(&mut self, value: Option<OSD>) {
self.day_cycle = value;
}
pub fn day_length(&self) -> i32 {
self.day_length
}
pub fn set_day_length(&mut self, value: i32) {
self.day_length = value;
}
pub fn day_offset(&self) -> i32 {
self.day_offset
}
pub fn set_day_offset(&mut self, value: i32) {
self.day_offset = value;
}
pub fn flags(&self) -> crate::messages::linden::EnvironmentFlags {
self.flags
}
pub fn set_flags(&mut self, value: crate::messages::linden::EnvironmentFlags) {
self.flags = value;
}
pub fn is_default(&self) -> bool {
self.is_default
}
pub fn set_is_default(&mut self, value: bool) {
self.is_default = value;
}
}
impl crate::interfaces::IMessage for EnvironmentData {
fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
self.deserialize(map)
}
fn serialize(&self) -> Result<OSDMap, Error> {
self.serialize()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtEnvironmentMessage {
environment: Option<EnvironmentData>,
message: String,
parcel_id: i32,
success: bool,
version: i32,
}
impl ExtEnvironmentMessage {
pub fn new() -> Result<Self, Error> {
Ok(Self {
environment: None,
message: String::new(),
parcel_id: -1,
success: false,
version: 0,
})
}
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
self.success = boolean(&map, "success", false);
self.parcel_id = integer(&map, "parcel_id", -1);
self.version = integer(&map, "version", 0);
self.message = string(&map, "message");
self.environment = match map.get("environment") {
Some(OSD::Map(values)) => {
let mut value = EnvironmentData::new()?;
value.deserialize(OSDMap::new_with_dictionary(values)?)?;
Some(value)
}
_ => None,
};
Ok(())
}
pub fn serialize(&self) -> Result<OSDMap, Error> {
let mut values = HashMap::new();
values.insert("parcel_id".into(), OSD::Integer(self.parcel_id));
if let Some(environment) = &self.environment {
values.insert(
"environment".into(),
OSD::Map(environment.serialize()?.snapshot()),
);
}
if self.version != 0 {
values.insert("version".into(), OSD::Integer(self.version));
}
OSDMap::new_with_dictionary(values)
}
pub fn environment(&self) -> Option<EnvironmentData> {
self.environment.clone()
}
pub fn set_environment(&mut self, value: Option<EnvironmentData>) {
self.environment = value;
}
pub fn message(&self) -> String {
self.message.clone()
}
pub fn set_message(&mut self, value: String) {
self.message = value;
}
pub fn parcel_id(&self) -> i32 {
self.parcel_id
}
pub fn set_parcel_id(&mut self, value: i32) {
self.parcel_id = value;
}
pub fn success(&self) -> bool {
self.success
}
pub fn set_success(&mut self, value: bool) {
self.success = value;
}
pub fn version(&self) -> i32 {
self.version
}
pub fn set_version(&mut self, value: i32) {
self.version = value;
}
}
impl crate::interfaces::IMessage for ExtEnvironmentMessage {
fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
self.deserialize(map)
}
fn serialize(&self) -> Result<OSDMap, Error> {
self.serialize()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LegacyEnvironmentMessage {
settings: Option<OSD>,
}
impl LegacyEnvironmentMessage {
pub fn new() -> Result<Self, Error> {
Ok(Self { settings: None })
}
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
self.settings = Some(OSD::Map(map.snapshot()));
Ok(())
}
pub fn serialize(&self) -> Result<OSDMap, Error> {
match &self.settings {
Some(OSD::Map(values)) => OSDMap::new_with_dictionary(values.clone()),
_ => OSDMap::new_with_constructor(),
}
}
pub fn settings(&self) -> Option<OSD> {
self.settings.clone()
}
pub fn set_settings(&mut self, value: Option<OSD>) {
self.settings = value;
}
}
impl crate::interfaces::IMessage for LegacyEnvironmentMessage {
fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
self.deserialize(map)
}
fn serialize(&self) -> Result<OSDMap, Error> {
self.serialize()
}
}
#[derive(Clone)]
pub struct RegionEnvironmentEventArgs {
environment: ExtEnvironmentMessage,
}
impl RegionEnvironmentEventArgs {
pub fn new(environment: ExtEnvironmentMessage) -> Result<Self, Error> {
Ok(Self { environment })
}
pub fn environment(&self) -> ExtEnvironmentMessage {
self.environment.clone()
}
}
#[derive(Clone)]
pub struct ParcelEnvironmentEventArgs {
parcel_id: i32,
environment: ExtEnvironmentMessage,
}
impl ParcelEnvironmentEventArgs {
pub fn new(parcel_id: i32, environment: ExtEnvironmentMessage) -> Result<Self, Error> {
Ok(Self {
parcel_id,
environment,
})
}
pub fn parcel_id(&self) -> i32 {
self.parcel_id
}
pub fn environment(&self) -> ExtEnvironmentMessage {
self.environment.clone()
}
}
#[derive(Clone)]
pub struct LegacyEnvironmentEventArgs {
environment: LegacyEnvironmentMessage,
}
impl LegacyEnvironmentEventArgs {
pub fn new(environment: LegacyEnvironmentMessage) -> Result<Self, Error> {
Ok(Self { environment })
}
pub fn environment(&self) -> LegacyEnvironmentMessage {
self.environment.clone()
}
}
pub(crate) struct EnvironmentManagerInner {
client: Arc<GridClient>,
disposed: AtomicBool,
region: RwLock<Option<ExtEnvironmentMessage>>,
legacy: RwLock<Option<LegacyEnvironmentMessage>>,
region_events: EventRegistry<RegionEnvironmentEventArgs>,
parcel_events: EventRegistry<ParcelEnvironmentEventArgs>,
legacy_events: EventRegistry<LegacyEnvironmentEventArgs>,
}
#[derive(Clone)]
pub struct EnvironmentManager {
pub(crate) inner: Arc<EnvironmentManagerInner>,
}
impl EnvironmentManager {
pub fn new(client: GridClient) -> Result<Self, Error> {
Self::native_new(Arc::new(client))
}
#[allow(clippy::unnecessary_wraps)] // The mapped public constructor has a fixed fallible shape.
pub(crate) fn native_new(client: Arc<GridClient>) -> Result<Self, Error> {
let candidate = Arc::new(EnvironmentManagerInner {
client: Arc::clone(&client),
disposed: AtomicBool::new(false),
region: RwLock::new(None),
legacy: RwLock::new(None),
region_events: EventRegistry::default(),
parcel_events: EventRegistry::default(),
legacy_events: EventRegistry::default(),
});
Ok(Self {
inner: client.install_environment_manager_inner(candidate),
})
}
async fn capability(
&self,
name: &str,
token: &CancellationToken,
) -> Result<Option<Uri>, Error> {
if self.inner.disposed.load(Ordering::Acquire) {
return Err(Error::InvalidOperation);
}
let Some(caps) = self
.inner
.client
.network()
.current_sim()
.and_then(|sim| sim.native_caps())
else {
return Ok(None);
};
if let Some(uri) = caps.capability_uri(name.to_owned())? {
return Ok(Some(uri));
}
if caps.seed_request_finished() {
return Ok(None);
}
let (sender, receiver) = oneshot::channel();
let sender = Arc::new(std::sync::Mutex::new(Some(sender)));
let wake = Arc::clone(&sender);
let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |_| {
if let Some(sender) = wake
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
let _ = sender.send(());
}
})));
if let Some(uri) = caps.capability_uri(name.to_owned())? {
return Ok(Some(uri));
}
let cancellation = token.cancelled();
pin_mut!(receiver, cancellation);
match select(receiver, cancellation).await {
Either::Left(_) => caps.capability_uri(name.to_owned()),
Either::Right(_) => Err(Error::Cancelled),
}
}
async fn request(
&self,
method: &str,
uri: Uri,
body: Vec<u8>,
token: Option<CancellationToken>,
) -> Result<Option<Vec<u8>>, Error> {
if body.len() > MAX_ENVIRONMENT_BYTES {
return Err(Error::Argument);
}
let http = self.inner.client.native_http_caps_client();
let token = token.unwrap_or_default();
let (response, bytes) = match method {
"GET" => http.get(uri, token, None).await?,
"POST" => {
http.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
body,
token,
None,
)
.await?
}
"DELETE" => {
http.delete_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
body,
token,
None,
)
.await?
}
_ => return Err(Error::Argument),
};
if bytes.len() > MAX_ENVIRONMENT_BYTES {
return Err(Error::Argument);
}
Ok((200..300).contains(&response.status_code).then_some(bytes))
}
fn parcel_uri(uri: Uri, parcel_id: i32) -> Uri {
let separator = if uri.0.contains('?') { '&' } else { '?' };
Uri(format!("{}{separator}parcelid={parcel_id}", uri.0))
}
fn decode_ext(bytes: Vec<u8>) -> Result<ExtEnvironmentMessage, Error> {
let OSD::Map(values) = OSDParser::deserialize_with_bytes(bytes)? else {
return Err(Error::Argument);
};
let mut message = ExtEnvironmentMessage::new()?;
message.deserialize(OSDMap::new_with_dictionary(values)?)?;
Ok(message)
}
pub async fn get_region_environment(
&self,
token: Option<CancellationToken>,
) -> Result<Option<ExtEnvironmentMessage>, Error> {
let resolved = token.clone().unwrap_or_default();
let Some(uri) = self.capability("ExtEnvironment", &resolved).await? else {
return Ok(None);
};
let Some(bytes) = self.request("GET", uri, Vec::new(), token).await? else {
return Ok(None);
};
let message = Self::decode_ext(bytes)?;
*write(&self.inner.region) = Some(message.clone());
self.inner
.region_events
.emit(RegionEnvironmentEventArgs::new(message.clone())?);
Ok(Some(message))
}
pub async fn get_parcel_environment(
&self,
parcel_id: i32,
token: Option<CancellationToken>,
) -> Result<Option<ExtEnvironmentMessage>, Error> {
let resolved = token.clone().unwrap_or_default();
let Some(uri) = self.capability("ExtEnvironment", &resolved).await? else {
return Ok(None);
};
let Some(bytes) = self
.request("GET", Self::parcel_uri(uri, parcel_id), Vec::new(), token)
.await?
else {
return Ok(None);
};
let message = Self::decode_ext(bytes)?;
self.inner
.parcel_events
.emit(ParcelEnvironmentEventArgs::new(parcel_id, message.clone())?);
Ok(Some(message))
}
async fn set_environment(
&self,
parcel_id: Option<i32>,
environment: EnvironmentData,
token: Option<CancellationToken>,
) -> Result<Option<ExtEnvironmentMessage>, Error> {
let resolved = token.clone().unwrap_or_default();
let Some(mut uri) = self.capability("ExtEnvironment", &resolved).await? else {
return Ok(None);
};
if let Some(parcel_id) = parcel_id {
uri = Self::parcel_uri(uri, parcel_id);
}
let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
"environment".into(),
OSD::Map(environment.serialize()?.snapshot()),
)])))?;
let Some(bytes) = self.request("POST", uri, body, token).await? else {
return Ok(None);
};
Self::decode_ext(bytes).map(Some)
}
pub async fn set_parcel_environment(
&self,
parcel_id: i32,
environment: EnvironmentData,
token: Option<CancellationToken>,
) -> Result<Option<ExtEnvironmentMessage>, Error> {
self.set_environment(Some(parcel_id), environment, token)
.await
}
pub async fn set_region_environment_with_environment_data_cancellation_token(
&self,
environment: EnvironmentData,
token: Option<CancellationToken>,
) -> Result<Option<ExtEnvironmentMessage>, Error> {
self.set_environment(None, environment, token).await
}
async fn reset_environment(
&self,
parcel_id: Option<i32>,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
let resolved = token.clone().unwrap_or_default();
let Some(mut uri) = self.capability("ExtEnvironment", &resolved).await? else {
return Ok(false);
};
if let Some(parcel_id) = parcel_id {
uri = Self::parcel_uri(uri, parcel_id);
}
let Some(bytes) = self.request("DELETE", uri, Vec::new(), token).await? else {
return Ok(false);
};
Ok(Self::decode_ext(bytes)?.success())
}
pub async fn reset_parcel_environment(
&self,
parcel_id: i32,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.reset_environment(Some(parcel_id), token).await
}
pub async fn reset_region_environment(
&self,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.reset_environment(None, token).await
}
pub async fn get_legacy_environment(
&self,
token: Option<CancellationToken>,
) -> Result<Option<LegacyEnvironmentMessage>, Error> {
let resolved = token.clone().unwrap_or_default();
let Some(uri) = self.capability("EnvironmentSettings", &resolved).await? else {
return Ok(None);
};
let Some(bytes) = self.request("GET", uri, Vec::new(), token).await? else {
return Ok(None);
};
let OSD::Map(values) = OSDParser::deserialize_with_bytes(bytes)? else {
return Err(Error::Argument);
};
let mut message = LegacyEnvironmentMessage::new()?;
message.deserialize(OSDMap::new_with_dictionary(values)?)?;
*write(&self.inner.legacy) = Some(message.clone());
self.inner
.legacy_events
.emit(LegacyEnvironmentEventArgs::new(message.clone())?);
Ok(Some(message))
}
pub async fn set_legacy_environment_with_osd_cancellation_token(
&self,
settings: OSD,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
let resolved = token.clone().unwrap_or_default();
let Some(uri) = self.capability("EnvironmentSettings", &resolved).await? else {
return Ok(false);
};
let body = OSDParser::serialize_llsd_xml_bytes(settings)?;
let Some(bytes) = self.request("POST", uri, body, token).await? else {
return Ok(false);
};
let OSD::Map(values) = OSDParser::deserialize_with_bytes(bytes)? else {
return Ok(false);
};
Ok(values
.get("success")
.and_then(|value| value.as_boolean().ok())
.unwrap_or(true))
}
pub fn region_environment(&self) -> Option<ExtEnvironmentMessage> {
read(&self.inner.region).clone()
}
pub fn set_region_environment(&mut self, value: Option<ExtEnvironmentMessage>) {
*write(&self.inner.region) = value;
}
pub fn legacy_environment(&self) -> Option<LegacyEnvironmentMessage> {
read(&self.inner.legacy).clone()
}
pub fn set_legacy_environment(&mut self, value: Option<LegacyEnvironmentMessage>) {
*write(&self.inner.legacy) = value;
}
pub fn subscribe_region_environment_updated(
&self,
handler: EventHandler<RegionEnvironmentEventArgs>,
) -> Subscription {
self.inner.region_events.subscribe(handler)
}
pub fn subscribe_parcel_environment_updated(
&self,
handler: EventHandler<ParcelEnvironmentEventArgs>,
) -> Subscription {
self.inner.parcel_events.subscribe(handler)
}
pub fn subscribe_legacy_environment_updated(
&self,
handler: EventHandler<LegacyEnvironmentEventArgs>,
) -> Subscription {
self.inner.legacy_events.subscribe(handler)
}
pub fn dispose(&self) -> Result<(), Error> {
self.inner.disposed.store(true, Ordering::Release);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn environment_defaults_and_wire_shape_match_eep() {
let mut value = EnvironmentData::new().unwrap();
assert_eq!(value.day_length(), 14_400);
assert_eq!(value.day_offset(), 57_600);
value.set_is_default(true);
let map = value.serialize().unwrap();
assert!(!map.contains_key("is_default".into()).unwrap());
assert!(!map.contains_key("day_cycle".into()).unwrap());
}
#[test]
fn ext_environment_round_trip_preserves_correlation_fields() {
let mut value = ExtEnvironmentMessage::new().unwrap();
value.set_parcel_id(17);
value.set_version(9);
value.set_environment(Some(EnvironmentData::new().unwrap()));
let mut decoded = ExtEnvironmentMessage::new().unwrap();
decoded.deserialize(value.serialize().unwrap()).unwrap();
assert_eq!(decoded.parcel_id(), 17);
assert_eq!(decoded.version(), 9);
assert!(decoded.environment().is_some());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@ mod caps_http;
mod client_core;
mod current_outfit;
mod download_manager;
mod environment_manager;
mod event_queue;
mod gesture;
mod initial_outfit;
@@ -48,11 +49,15 @@ mod object_model;
#[rustfmt::skip]
pub mod packet_catalog;
mod packet_wire;
mod parcel_manager;
mod sim_stats;
mod skeleton;
mod sound_manager;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod skeleton_catalog;
mod targa;
mod terrain_codec;
mod terrain_manager;
mod transfers;
mod udp_transport;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.

View File

@@ -22,8 +22,8 @@ use crate::udp_transport::{UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransp
use crate::{
AccountLevelBenefits, Avatar, Caps, Error, GenericStreamingMethod, GridClient, Helpers,
LoginCredential, LoginParams, LoginProgressEventArgs, LoginResponseData, LoginStatus,
NetworkManagerLoginResponseCallback, Primitive, RegionFlags, RegionProtocols, SimAccess,
SimulatorDataPool, SimulatorFeatures, SimulatorSimStats, TerrainPatch,
NetworkManagerLoginResponseCallback, Parcel, Primitive, RegionFlags, RegionProtocols,
SimAccess, SimulatorDataPool, SimulatorFeatures, SimulatorSimStats, TerrainPatch,
};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDMap, OSDParser};
use libremetaverse_types::compat::{
@@ -978,8 +978,11 @@ pub struct SimulatorData {
pub name: String,
pub objects_avatars: RwLock<HashMap<u32, Avatar>>,
pub objects_primitives: RwLock<HashMap<u32, Primitive>>,
pub parcel_overlay: Vec<u8>,
pub parcel_overlays_received: i32,
pub parcel_overlay: RwLock<Vec<u8>>,
pub parcel_overlays_received: AtomicI32,
parcel_overlay_segments: AtomicU8,
pub parcels: RwLock<HashMap<i32, Parcel>>,
pub parcel_map: RwLock<Vec<i32>>,
pub product_name: String,
pub product_sku: String,
pub protocols: RegionProtocols,
@@ -990,7 +993,7 @@ pub struct SimulatorData {
pub size_x: u32,
pub size_y: u32,
pub stats: SimulatorSimStats,
pub terrain: Vec<TerrainPatch>,
pub terrain: RwLock<Vec<TerrainPatch>>,
pub terrain_base0: UUID,
pub terrain_base1: UUID,
pub terrain_base2: UUID,
@@ -1008,7 +1011,7 @@ pub struct SimulatorData {
pub terrain_start_height10: f32,
pub terrain_start_height11: f32,
pub water_height: f32,
pub wind_speeds: Option<Vec<Vector2>>,
pub wind_speeds: RwLock<Option<Vec<Vector2>>>,
endpoint: std::net::SocketAddr,
connected: AtomicBool,
movement_complete: AtomicBool,
@@ -1141,8 +1144,11 @@ impl Simulator {
name: String::new(),
objects_avatars: RwLock::new(HashMap::new()),
objects_primitives: RwLock::new(HashMap::new()),
parcel_overlay: vec![0; 4096],
parcel_overlays_received: 0,
parcel_overlay: RwLock::new(vec![0; 4096]),
parcel_overlays_received: AtomicI32::new(0),
parcel_overlay_segments: AtomicU8::new(0),
parcels: RwLock::new(HashMap::new()),
parcel_map: RwLock::new(vec![0; 4096]),
product_name: String::new(),
product_sku: String::new(),
protocols: RegionProtocols(0),
@@ -1153,7 +1159,7 @@ impl Simulator {
size_x: size_x.unwrap_or(Self::DEFAULT_REGION_SIZE_X),
size_y: size_y.unwrap_or(Self::DEFAULT_REGION_SIZE_Y),
stats: SimulatorSimStats::default(),
terrain: Vec::new(),
terrain: RwLock::new(Vec::new()),
terrain_base0: UUID::zero(),
terrain_base1: UUID::zero(),
terrain_base2: UUID::zero(),
@@ -1171,7 +1177,7 @@ impl Simulator {
terrain_start_height10: 0.0,
terrain_start_height11: 0.0,
water_height: 0.0,
wind_speeds: None,
wind_speeds: RwLock::new(None),
endpoint: address,
connected: AtomicBool::new(false),
movement_complete: AtomicBool::new(false),
@@ -1220,6 +1226,187 @@ impl Simulator {
self.data.id
}
pub(crate) fn native_store_terrain_patch(
&self,
patch: TerrainPatch,
large_region: bool,
) -> Result<(), Error> {
let (width, height) = if large_region {
(
usize::try_from(self.size_x.max(16) / 16).map_err(|_| Error::Argument)?,
usize::try_from(self.size_y.max(16) / 16).map_err(|_| Error::Argument)?,
)
} else {
(16, 16)
};
let x = usize::try_from(patch.x).map_err(|_| Error::Argument)?;
let y = usize::try_from(patch.y).map_err(|_| Error::Argument)?;
if x >= width || y >= height || width > 256 || height > 256 {
return Err(Error::Argument);
}
let index = y
.checked_mul(width)
.and_then(|value| value.checked_add(x))
.ok_or(Error::Argument)?;
let patch_count = width.checked_mul(height).ok_or(Error::Argument)?;
let mut terrain = write(&self.terrain);
terrain.resize_with(patch_count, || TerrainPatch {
data: Vec::new(),
x: 0,
y: 0,
});
terrain[index] = patch;
Ok(())
}
pub(crate) fn native_store_wind(&self, values: Vec<Vector2>) {
*write(&self.wind_speeds) = Some(values);
}
pub(crate) fn native_apply_parcel_overlay(
&self,
sequence_id: i32,
data: Vec<u8>,
) -> Result<bool, Error> {
if !(0..4).contains(&sequence_id) {
return Err(Error::Argument);
}
let mut overlay = write(&self.parcel_overlay);
let segment_size = overlay.len().checked_div(4).ok_or(Error::Argument)?;
if data.len() != segment_size {
return Err(Error::Argument);
}
let sequence = usize::try_from(sequence_id).map_err(|_| Error::Argument)?;
let offset = sequence.checked_mul(segment_size).ok_or(Error::Argument)?;
let end = offset.checked_add(segment_size).ok_or(Error::Argument)?;
overlay[offset..end].copy_from_slice(&data);
// The overlay may be retransmitted or arrive out of order. Count unique
// sequence IDs so duplicates cannot publish an incomplete map.
let bit = 1_u8 << sequence;
let old_segments = self.parcel_overlay_segments.load(Ordering::Acquire);
let segments = old_segments | bit;
self.parcel_overlay_segments
.store(segments, Ordering::Release);
let received = i32::try_from(segments.count_ones()).map_err(|_| Error::Argument)?;
self.parcel_overlays_received
.store(received, Ordering::Release);
if segments == 0b1111 {
self.parcel_overlay_segments.store(0, Ordering::Release);
self.parcel_overlays_received.store(0, Ordering::Release);
Ok(true)
} else {
Ok(false)
}
}
pub(crate) fn native_store_parcel(&self, parcel: Parcel) {
if parcel.bitmap.len() == 512 {
let mut map = write(&self.parcel_map);
for cell in 0..4096 {
if parcel.bitmap[cell / 8] & (1 << (cell % 8)) != 0 {
map[cell] = parcel.local_id;
}
}
}
write(&self.parcels).insert(parcel.local_id, parcel);
}
pub(crate) fn native_parcel(&self, local_id: i32) -> Option<Parcel> {
read(&self.parcels).get(&local_id).cloned()
}
pub(crate) fn native_parcel_map_at(&self, x: usize, y: usize) -> Option<i32> {
if x >= 64 || y >= 64 {
return None;
}
read(&self.parcel_map).get(y * 64 + x).copied()
}
pub(crate) fn native_parcels_snapshot(&self) -> HashMap<i32, Parcel> {
read(&self.parcels).clone()
}
pub(crate) fn native_parcel_map_snapshot(&self) -> Vec<i32> {
read(&self.parcel_map).clone()
}
pub(crate) fn native_is_parcel_map_full(&self) -> bool {
read(&self.parcel_map).iter().all(|value| *value > 0)
}
pub(crate) fn native_terrain_height_at_point(
&self,
x: i32,
y: i32,
height: &mut f32,
) -> Result<bool, Error> {
let (Ok(x), Ok(y)) = (u32::try_from(x), u32::try_from(y)) else {
return Ok(false);
};
if x >= self.size_x || y >= self.size_y {
return Ok(false);
}
let per_edge = usize::try_from(self.size_x.max(16) / 16).map_err(|_| Error::Argument)?;
let patch_x = usize::try_from(x / 16).map_err(|_| Error::Argument)?;
let patch_y = usize::try_from(y / 16).map_err(|_| Error::Argument)?;
let point = usize::try_from((y % 16) * 16 + (x % 16)).map_err(|_| Error::Argument)?;
let terrain = read(&self.terrain);
let Some(patch) = terrain.get(patch_y * per_edge + patch_x) else {
return Ok(false);
};
let Some(value) = patch.data.get(point) else {
return Ok(false);
};
*height = *value;
Ok(true)
}
pub(crate) fn native_clear_parcels(&self) {
write(&self.parcels).clear();
write(&self.parcel_map).fill(0);
}
pub(crate) fn native_update_parcel_dwell(&self, local_id: i32, dwell: f32) {
if let Some(parcel) = write(&self.parcels).get_mut(&local_id) {
parcel.dwell = dwell;
}
}
pub(crate) fn native_update_parcel_access(
&self,
local_id: i32,
flags: u32,
entries: Vec<crate::ParcelManagerParcelAccessEntry>,
) {
if let Some(parcel) = write(&self.parcels).get_mut(&local_id) {
if flags & crate::AccessList::BAN.0 != 0 {
parcel.access_black_list = entries;
} else {
parcel.access_white_list = entries;
}
}
}
pub(crate) fn native_send_parcel_clean_time(
&self,
local_id: i32,
clean_time: i32,
) -> Result<(), Error> {
let mut packet = crate::packets::ParcelSetOtherCleanTimePacket::new_with_constructor()?;
packet.agent_data.agent_id = *read(&self.data.agent_id);
packet.agent_data.session_id = *read(&self.data.session_id);
packet.parcel_data.local_id = local_id;
packet.parcel_data.other_clean_time = clean_time;
let data = packet.to_bytes_with_method()?;
self.native_send_packet_data(
data.clone(),
i32::try_from(data.len()).map_err(|_| Error::Argument)?,
PacketType::ParcelSetOtherCleanTime,
false,
)
}
pub(crate) fn native_from_weak(data: &Weak<SimulatorData>) -> Option<Self> {
data.upgrade().map(|data| Self { caps: None, data })
}

File diff suppressed because it is too large Load Diff

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)))
);
}
}

View File

@@ -0,0 +1,578 @@
//! Second Life terrain patch codec and layer packet construction.
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_wrap)] // Bit widths and bounded patch coordinates are range-checked.
#![allow(clippy::cast_sign_loss)] // Patch coordinates are validated before indexing.
use crate::{BitPack, Error, TerrainPatchLayerType};
const PATCH_SIZE: usize = 16;
const PATCH_VALUES: usize = PATCH_SIZE * PATCH_SIZE;
const STRIDE: i32 = 264;
const OO_SQRT2: f32 = std::f32::consts::FRAC_1_SQRT_2;
#[derive(Clone, Debug, PartialEq)]
pub struct TerrainPatch {
pub data: Vec<f32>,
pub x: i32,
pub y: i32,
}
impl TerrainPatch {
pub fn new() -> Result<Self, Error> {
Ok(Self {
data: Vec::new(),
x: 0,
y: 0,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TerrainPatchGroupHeader {
pub stride: i32,
pub patch_size: i32,
pub type_: TerrainPatchLayerType,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TerrainPatchHeader {
pub dc_offset: f32,
pub range: i32,
pub quant_w_bits: i32,
pub patch_i_ds: i32,
pub word_bits: u32,
large_region: bool,
}
impl Default for TerrainPatchHeader {
fn default() -> Self {
Self {
dc_offset: 0.0,
range: 0,
quant_w_bits: 0,
patch_i_ds: 0,
word_bits: 0,
large_region: false,
}
}
}
impl TerrainPatchHeader {
pub fn set_patch_i_ds(&mut self, xx: i32, yy: i32) -> Result<i32, Error> {
let valid = if self.large_region {
(0..=65_535).contains(&xx) && (0..=65_535).contains(&yy)
} else {
(0..=31).contains(&xx) && (0..=31).contains(&yy)
};
if !valid {
return Err(Error::Argument);
}
self.patch_i_ds = if self.large_region {
(xx << 16) | (yy & 0xffff)
} else {
(xx << 5) | (yy & 0x1f)
};
Ok(self.patch_i_ds)
}
pub fn large_region(&self) -> bool {
self.large_region
}
pub fn set_large_region(&mut self, value: bool) {
self.large_region = value;
}
pub fn x(&self) -> i32 {
self.patch_i_ds >> if self.large_region { 16 } else { 5 }
}
pub fn set_x(&mut self, value: i32) {
let _ = self.set_patch_i_ds(value, self.y());
}
pub fn y(&self) -> i32 {
self.patch_i_ds & if self.large_region { 0xffff } else { 0x1f }
}
pub fn set_y(&mut self, value: i32) {
let _ = self.set_patch_i_ds(self.x(), value);
}
}
pub struct TerrainCompressor;
impl TerrainCompressor {
pub const PATCHES_PER_EDGE: i32 = 16;
pub const END_OF_PATCHES: i32 = 97;
fn copy_matrix() -> [usize; PATCH_VALUES] {
let mut result = [0; PATCH_VALUES];
let (mut diag, mut right, mut i, mut j, mut count) = (false, true, 0usize, 0usize, 0usize);
while i < PATCH_SIZE && j < PATCH_SIZE {
result[j * PATCH_SIZE + i] = count;
count += 1;
if !diag {
if right {
if i < PATCH_SIZE - 1 {
i += 1;
} else {
j += 1;
}
} else if j < PATCH_SIZE - 1 {
j += 1;
} else {
i += 1;
}
right = !right;
diag = true;
} else if right {
i += 1;
j -= 1;
if i == PATCH_SIZE - 1 || j == 0 {
diag = false;
}
} else {
i -= 1;
j += 1;
if j == PATCH_SIZE - 1 || i == 0 {
diag = false;
}
}
}
result
}
fn cosine(u: usize, n: usize) -> f32 {
(((2 * n + 1) as f32) * (u as f32) * std::f32::consts::PI * 0.5 / PATCH_SIZE as f32).cos()
}
fn prescan(data: &[f32]) -> Result<TerrainPatchHeader, Error> {
if data.len() != PATCH_VALUES || data.iter().any(|value| !value.is_finite()) {
return Err(Error::Argument);
}
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for value in data {
min = min.min(*value);
max = max.max(*value);
}
Ok(TerrainPatchHeader {
dc_offset: min,
range: ((max - min) + 1.0) as i32,
..TerrainPatchHeader::default()
})
}
fn compress(data: &[f32], header: &TerrainPatchHeader) -> [i32; PATCH_VALUES] {
let premult = 1024.0 / header.range.max(1) as f32;
let sub = 512.0 + header.dc_offset * premult;
let mut block = [0.0; PATCH_VALUES];
for (target, value) in block.iter_mut().zip(data) {
*target = *value * premult - sub;
}
let mut rows = [0.0; PATCH_VALUES];
for line in 0..PATCH_SIZE {
let base = line * PATCH_SIZE;
let total: f32 = block[base..base + PATCH_SIZE].iter().sum();
rows[base] = OO_SQRT2 * total;
for u in 1..PATCH_SIZE {
rows[base + u] = (0..PATCH_SIZE)
.map(|n| block[base + n] * Self::cosine(u, n))
.sum();
}
}
let copy = Self::copy_matrix();
let mut output = [0; PATCH_VALUES];
for column in 0..PATCH_SIZE {
let total: f32 = (0..PATCH_SIZE).map(|n| rows[n * PATCH_SIZE + column]).sum();
output[copy[column]] = (OO_SQRT2 * total * 0.125 / (1.0 + 2.0 * column as f32)) as i32;
for u in 1..PATCH_SIZE {
let total: f32 = (0..PATCH_SIZE)
.map(|n| rows[n * PATCH_SIZE + column] * Self::cosine(u, n))
.sum();
output[copy[u * PATCH_SIZE + column]] =
(total * 0.125 / (1.0 + 2.0 * (u + column) as f32)) as i32;
}
}
output
}
fn encode_header(
output: &mut BitPack,
mut header: TerrainPatchHeader,
patch: &[i32],
) -> Result<i32, Error> {
let base = (header.quant_w_bits & 0x0f) + 2;
let mut word_bits = base / 2;
for value in patch.iter().copied().filter(|value| *value != 0) {
let magnitude = value.unsigned_abs();
let bits = (u32::BITS - magnitude.leading_zeros()) as i32;
word_bits = word_bits.max(bits);
}
word_bits = (word_bits + 1).clamp(2, 17);
header.quant_w_bits = (header.quant_w_bits & 0xf0) | (word_bits - 2);
output.pack_bits_with_int32_int32(header.quant_w_bits, 8)?;
output.pack_float(header.dc_offset)?;
output.pack_bits_with_int32_int32(header.range, 16)?;
output.pack_bits_with_int32_int32(
header.patch_i_ds,
if header.large_region { 32 } else { 10 },
)?;
Ok(word_bits)
}
fn encode_patch_values(
output: &mut BitPack,
patch: &[i32],
word_bits: i32,
) -> Result<(), Error> {
for (index, value) in patch.iter().copied().enumerate() {
if value == 0 {
if patch[index..].iter().all(|candidate| *candidate == 0) {
output.pack_bits_with_int32_int32(2, 2)?;
return Ok(());
}
output.pack_bits_with_int32_int32(0, 1)?;
} else {
output.pack_bits_with_int32_int32(if value < 0 { 7 } else { 6 }, 3)?;
let maximum = (1_i32 << word_bits) - 1;
output.pack_bits_with_int32_int32(
i32::try_from(value.unsigned_abs())
.unwrap_or(maximum)
.min(maximum),
word_bits,
)?;
}
}
Ok(())
}
fn encode_one(
output: &mut BitPack,
data: &[f32],
x: i32,
y: i32,
large_region: bool,
) -> Result<(), Error> {
let mut header = Self::prescan(data)?;
header.large_region = large_region;
header.quant_w_bits = 136;
header.set_patch_i_ds(x, y)?;
let patch = Self::compress(data, &header);
let word_bits = Self::encode_header(output, header, &patch)?;
Self::encode_patch_values(output, &patch, word_bits)
}
pub fn create_layer_data_packet(
patches: Vec<TerrainPatch>,
type_: TerrainPatchLayerType,
) -> Result<crate::packets::LayerDataPacket, Error> {
if patches.len() > 4096 {
return Err(Error::Argument);
}
let capacity = patches
.len()
.checked_mul(1024)
.and_then(|value| value.checked_add(32))
.ok_or(Error::Argument)?;
let mut bits = BitPack::new(vec![0; capacity], 0)?;
bits.pack_bits_with_int32_int32(STRIDE, 16)?;
bits.pack_bits_with_int32_int32(PATCH_SIZE as i32, 8)?;
bits.pack_bits_with_int32_int32(i32::from(type_.0), 8)?;
let large = matches!(type_.0, 57 | 58 | 77);
for patch in patches {
Self::encode_one(&mut bits, &patch.data, patch.x, patch.y, large)?;
}
bits.pack_bits_with_int32_int32(Self::END_OF_PATCHES, 8)?;
let used = usize::try_from(bits.byte_pos())
.map_err(|_| Error::Argument)?
.checked_add(1)
.ok_or(Error::Argument)?;
let mut packet = crate::packets::LayerDataPacket::new_with_constructor()?;
packet.layer_id.type_ = type_.0;
packet.layer_data.data = bits.data[..used.min(bits.data.len())].to_vec();
Ok(packet)
}
pub fn create_land_packet_with_single_array_int32_int32(
patch_data: Vec<f32>,
x: i32,
y: i32,
) -> Result<crate::packets::LayerDataPacket, Error> {
Self::create_layer_data_packet(
vec![TerrainPatch {
data: patch_data,
x,
y,
}],
TerrainPatchLayerType::LAND,
)
}
pub fn create_land_packet_with_single_array_int32_int32_80090bdf(
patch_data: Vec<f32>,
x: i32,
y: i32,
) -> Result<crate::packets::LayerDataPacket, Error> {
Self::create_land_packet_with_single_array_int32_int32(patch_data, x, y)
}
pub fn create_land_packet_with_single_array_int32_array(
heightmap: Vec<f32>,
patches: Vec<i32>,
) -> Result<crate::packets::LayerDataPacket, Error> {
let edge = (heightmap.len() as f64).sqrt() as usize;
if edge * edge != heightmap.len()
|| edge < PATCH_SIZE
|| !edge.is_multiple_of(PATCH_SIZE)
|| patches.len() > 4096
{
return Err(Error::Argument);
}
let per_edge = edge / PATCH_SIZE;
let mut values = Vec::with_capacity(patches.len());
for id in patches {
let id = usize::try_from(id).map_err(|_| Error::Argument)?;
let (x, y) = (id % per_edge, id / per_edge);
if y >= per_edge {
return Err(Error::Argument);
}
let mut data = Vec::with_capacity(PATCH_VALUES);
for row in 0..PATCH_SIZE {
let start = (y * PATCH_SIZE + row) * edge + x * PATCH_SIZE;
data.extend_from_slice(&heightmap[start..start + PATCH_SIZE]);
}
values.push(TerrainPatch {
data,
x: x as i32,
y: y as i32,
});
}
let layer = if edge > 256 {
TerrainPatchLayerType::LAND_EXTENDED
} else {
TerrainPatchLayerType::LAND
};
Self::create_layer_data_packet(values, layer)
}
pub fn create_patch_with_bit_pack_single_array_int32_int32(
mut output: BitPack,
patch_data: Vec<f32>,
x: i32,
y: i32,
) -> Result<(), Error> {
Self::encode_one(&mut output, &patch_data, x, y, false)
}
pub fn create_patch_with_bit_pack_single_array_int32_int32_boolean(
mut output: BitPack,
patch_data: Vec<f32>,
x: i32,
y: i32,
large_region: bool,
) -> Result<(), Error> {
Self::encode_one(&mut output, &patch_data, x, y, large_region)
}
pub fn create_patch_from_heightmap_with_bit_pack_single_array_int32_int32(
output: BitPack,
heightmap: Vec<f32>,
x: i32,
y: i32,
) -> Result<(), Error> {
Self::create_patch_from_heightmap_with_bit_pack_single_array_int32_int32_boolean(
output, heightmap, x, y, false,
)
}
pub fn create_patch_from_heightmap_with_bit_pack_single_array_int32_int32_boolean(
mut output: BitPack,
heightmap: Vec<f32>,
x: i32,
y: i32,
large_region: bool,
) -> Result<(), Error> {
if heightmap.len() != 65_536 || !(0..16).contains(&x) || !(0..16).contains(&y) {
return Err(Error::Argument);
}
let mut data = Vec::with_capacity(PATCH_VALUES);
for row in 0..PATCH_SIZE {
let start = (y as usize * PATCH_SIZE + row) * 256 + x as usize * PATCH_SIZE;
data.extend_from_slice(&heightmap[start..start + PATCH_SIZE]);
}
Self::encode_one(&mut output, &data, x, y, large_region)
}
pub fn decode_patch_header_with_bit_pack(
bitpack: BitPack,
) -> Result<TerrainPatchHeader, Error> {
Self::decode_patch_header_with_bit_pack_boolean(bitpack, false)
}
pub fn decode_patch_header_with_bit_pack_boolean(
mut bitpack: BitPack,
large_region: bool,
) -> Result<TerrainPatchHeader, Error> {
Self::decode_header(&mut bitpack, large_region)
}
pub(crate) fn decode_header(
bitpack: &mut BitPack,
large_region: bool,
) -> Result<TerrainPatchHeader, Error> {
let quant_w_bits = bitpack.unpack_bits(8)?;
if quant_w_bits == Self::END_OF_PATCHES {
return Ok(TerrainPatchHeader {
quant_w_bits,
large_region,
..TerrainPatchHeader::default()
});
}
let dc_offset = bitpack.unpack_float()?;
let range = bitpack.unpack_bits(16)?;
let patch_i_ds = bitpack.unpack_bits(if large_region { 32 } else { 10 })?;
Ok(TerrainPatchHeader {
dc_offset,
range,
quant_w_bits,
patch_i_ds,
word_bits: ((quant_w_bits & 0x0f) + 2) as u32,
large_region,
})
}
pub fn decode_patch(
mut patches: Vec<i32>,
mut bitpack: BitPack,
header: TerrainPatchHeader,
size: i32,
) -> Result<(), Error> {
Self::decode_values(&mut patches, &mut bitpack, header, size)
}
pub(crate) fn decode_values(
patches: &mut [i32],
bitpack: &mut BitPack,
header: TerrainPatchHeader,
size: i32,
) -> Result<(), Error> {
let count = usize::try_from(size.checked_mul(size).ok_or(Error::Argument)?)
.map_err(|_| Error::Argument)?;
if patches.len() < count
|| !(1..=32).contains(&size)
|| !(2..=17).contains(&header.word_bits)
{
return Err(Error::Argument);
}
for index in 0..count {
if bitpack.unpack_bits(1)? == 0 {
patches[index] = 0;
continue;
}
if bitpack.unpack_bits(1)? == 0 {
patches[index..count].fill(0);
break;
}
let negative = bitpack.unpack_bits(1)? != 0;
let value = bitpack.unpack_bits(header.word_bits as i32)?;
patches[index] = if negative { -value } else { value };
}
Ok(())
}
pub fn decompress_patch(
patches: Vec<i32>,
header: TerrainPatchHeader,
group: TerrainPatchGroupHeader,
) -> Result<Vec<f32>, Error> {
if group.patch_size != 16 || patches.len() < PATCH_VALUES {
return Err(Error::Argument);
}
let copy = Self::copy_matrix();
let mut block = [0.0; PATCH_VALUES];
for index in 0..PATCH_VALUES {
block[index] = patches[copy[index]] as f32
* (1.0 + 2.0 * ((index / PATCH_SIZE) + (index % PATCH_SIZE)) as f32);
}
let mut columns = [0.0; PATCH_VALUES];
for column in 0..PATCH_SIZE {
for n in 0..PATCH_SIZE {
columns[n * PATCH_SIZE + column] = OO_SQRT2 * block[column]
+ (1..PATCH_SIZE)
.map(|u| block[u * PATCH_SIZE + column] * Self::cosine(u, n))
.sum::<f32>();
}
}
let mut transformed = [0.0; PATCH_VALUES];
for line in 0..PATCH_SIZE {
let base = line * PATCH_SIZE;
for n in 0..PATCH_SIZE {
transformed[base + n] = (OO_SQRT2 * columns[base]
+ (1..PATCH_SIZE)
.map(|u| columns[base + u] * Self::cosine(u, n))
.sum::<f32>())
* 0.125;
}
}
let prequant = (header.quant_w_bits >> 4) + 2;
if !(1..31).contains(&prequant) {
return Err(Error::Argument);
}
let mult = header.range as f32 / (1_i32 << prequant) as f32;
let add = mult * (1_i32 << (prequant - 1)) as f32 + header.dc_offset;
Ok(transformed
.into_iter()
.map(|value| value * mult + add)
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terrain_codec_round_trip_stays_within_quantization_error() {
let source: Vec<f32> = (0..PATCH_VALUES)
.map(|index| 20.0 + index as f32 * 0.03125)
.collect();
let packet = TerrainCompressor::create_land_packet_with_single_array_int32_int32(
source.clone(),
3,
7,
)
.unwrap();
let mut bits = BitPack::new(packet.layer_data.data, 0).unwrap();
assert_eq!(bits.unpack_bits(16).unwrap(), STRIDE);
let size = bits.unpack_bits(8).unwrap();
assert_eq!(
bits.unpack_bits(8).unwrap(),
i32::from(TerrainPatchLayerType::LAND.0)
);
let header = TerrainCompressor::decode_header(&mut bits, false).unwrap();
let mut coefficients = vec![0; PATCH_VALUES];
TerrainCompressor::decode_values(&mut coefficients, &mut bits, header, size).unwrap();
let decoded = TerrainCompressor::decompress_patch(
coefficients,
header,
TerrainPatchGroupHeader {
stride: STRIDE,
patch_size: size,
type_: TerrainPatchLayerType::LAND,
},
)
.unwrap();
assert_eq!((header.x(), header.y()), (3, 7));
assert!(
source
.iter()
.zip(decoded)
.all(|(expected, actual)| (expected - actual).abs() < 0.2)
);
}
#[test]
fn terrain_payloads_are_bounded_and_coordinates_validated() {
assert!(
TerrainCompressor::create_land_packet_with_single_array_int32_int32(
vec![0.0; 255],
0,
0
)
.is_err()
);
assert!(
TerrainCompressor::create_land_packet_with_single_array_int32_int32(
vec![0.0; 256],
32,
0
)
.is_err()
);
}
}

View File

@@ -0,0 +1,464 @@
//! Terrain layer reception, wind state, and PBR terrain material capabilities.
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::unnecessary_wraps)] // Mapped constructors have fixed fallible signatures.
use crate::agent_manager::EventRegistry;
use crate::packet_catalog::{GeneratedPacket, PacketType};
use crate::terrain_codec::{TerrainCompressor, TerrainPatch, TerrainPatchGroupHeader};
use crate::{BitPack, Error, GridClient, Simulator, TerrainPatchLayerType};
use futures_channel::oneshot;
use futures_util::future::{Either, select};
use futures_util::pin_mut;
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::Vector2;
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
const MAX_LAYER_BYTES: usize = 16 * 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 LandPatchReceivedEventArgs {
simulator: Simulator,
x: i32,
y: i32,
patch_size: i32,
height_map: Vec<f32>,
}
impl LandPatchReceivedEventArgs {
pub fn new(
simulator: Simulator,
x: i32,
y: i32,
patch_size: i32,
height_map: Vec<f32>,
) -> Result<Self, Error> {
if height_map.len()
!= usize::try_from(patch_size.checked_mul(patch_size).ok_or(Error::Argument)?)
.map_err(|_| Error::Argument)?
{
return Err(Error::Argument);
}
Ok(Self {
simulator,
x,
y,
patch_size,
height_map,
})
}
pub fn simulator(&self) -> Simulator {
self.simulator.clone()
}
pub fn x(&self) -> i32 {
self.x
}
pub fn y(&self) -> i32 {
self.y
}
pub fn patch_size(&self) -> i32 {
self.patch_size
}
pub fn height_map(&self) -> Vec<f32> {
self.height_map.clone()
}
}
pub(crate) struct TerrainManagerInner {
client: Arc<GridClient>,
events: EventRegistry<LandPatchReceivedEventArgs>,
raw_subscription: Mutex<Option<Subscription>>,
disposed: AtomicBool,
}
#[derive(Clone)]
pub struct TerrainManager {
pub(crate) inner: Arc<TerrainManagerInner>,
}
impl TerrainManager {
pub const TERRAIN_MATERIAL_SLOT_COUNT: i32 = 4;
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_terrain_manager_inner() {
return Ok(Self { inner });
}
let inner = Arc::new(TerrainManagerInner {
client: Arc::clone(&client),
events: EventRegistry::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_layer(event);
}
},
)));
Ok(Self {
inner: client.install_terrain_manager_inner(inner),
})
}
pub fn subscribe_land_patch_received(
&self,
handler: EventHandler<LandPatchReceivedEventArgs>,
) -> Subscription {
self.inner.events.subscribe(handler)
}
pub fn dispose(&self) -> Result<(), Error> {
self.inner.disposed.store(true, Ordering::Release);
mutex(&self.inner.raw_subscription).take();
Ok(())
}
async fn capability(
&self,
name: &str,
token: &CancellationToken,
) -> Result<Option<Uri>, Error> {
if self.inner.disposed.load(Ordering::Acquire) {
return Err(Error::InvalidOperation);
}
let Some(caps) = self
.inner
.client
.network()
.current_sim()
.and_then(|sim| sim.native_caps())
else {
return Ok(None);
};
if let Some(uri) = caps.capability_uri(name.into())? {
return Ok(Some(uri));
}
if caps.seed_request_finished() {
return Ok(None);
}
let (sender, receiver) = oneshot::channel();
let sender = Arc::new(Mutex::new(Some(sender)));
let wake = Arc::clone(&sender);
let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |_| {
if let Some(sender) = mutex(&wake).take() {
let _ = sender.send(());
}
})));
if let Some(uri) = caps.capability_uri(name.into())? {
return Ok(Some(uri));
}
let cancellation = token.cancelled();
pin_mut!(receiver, cancellation);
match select(receiver, cancellation).await {
Either::Left(_) => caps.capability_uri(name.into()),
Either::Right(_) => Err(Error::Cancelled),
}
}
pub async fn get_terrain_material_overrides(
&self,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<Vec<Option<crate::assets::AssetMaterial>>>, Error> {
let token = cancellation_token.unwrap_or_default();
let Some(uri) = self.capability("ModifyRegion", &token).await? else {
return Ok(None);
};
let (response, bytes) = self
.inner
.client
.native_http_caps_client()
.get(uri, token, None)
.await?;
if !(200..300).contains(&response.status_code) || bytes.len() > MAX_LAYER_BYTES {
return Ok(None);
}
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
return Ok(None);
};
if !map
.get("success")
.and_then(|value| value.as_boolean().ok())
.unwrap_or(false)
{
return Ok(None);
}
let Some(OSD::Array(overrides)) = map.get("overrides") else {
return Ok(None);
};
if overrides.len() < 4 || overrides.len() > 1024 {
return Ok(None);
}
let mut result = Vec::with_capacity(4);
for value in overrides.iter().take(4) {
match value {
OSD::Map(values) if !values.is_empty() => {
result.push(Some(crate::assets::AssetMaterial::from_override_osd(
OSDMap::new_with_dictionary(values.clone())?,
)?));
}
_ => result.push(None),
}
}
Ok(Some(result))
}
pub async fn set_terrain_material_overrides(
&self,
overrides: Vec<Option<crate::assets::AssetMaterial>>,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
if overrides.len() != 4 {
return Err(Error::Argument);
}
let token = cancellation_token.unwrap_or_default();
let Some(uri) = self.capability("ModifyRegion", &token).await? else {
return Ok(false);
};
let mut wire = Vec::with_capacity(4);
for material in overrides {
wire.push(match material {
Some(value) => OSD::Map(value.to_override_osd()?.snapshot()),
None => OSD::Map(HashMap::new()),
});
}
let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
"overrides".into(),
OSD::Array(wire),
)])))?;
if body.len() > MAX_LAYER_BYTES {
return Err(Error::Argument);
}
let (response, bytes) = self
.inner
.client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
body,
token,
None,
)
.await?;
if !(200..300).contains(&response.status_code) || bytes.len() > MAX_LAYER_BYTES {
return Ok(false);
}
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
return Ok(false);
};
Ok(map
.get("success")
.and_then(|value| value.as_boolean().ok())
.unwrap_or(false))
}
}
impl TerrainManagerInner {
fn handle_layer(&self, event: crate::network_manager::RawPacketReceivedEventArgs) {
if self.disposed.load(Ordering::Acquire)
|| event.packet_type != PacketType::LayerData
|| event.data.len() > MAX_LAYER_BYTES
{
return;
}
let mut position = 0;
let Ok(packet) =
crate::packets::LayerDataPacket::new_from_bytes(&event.data, &mut position)
else {
return;
};
let _ = self.decode_layer(packet, event.simulator);
}
fn decode_layer(
&self,
packet: crate::packets::LayerDataPacket,
simulator: Simulator,
) -> Result<(), Error> {
if packet.layer_data.data.len() > MAX_LAYER_BYTES {
return Err(Error::Argument);
}
let mut bits = BitPack::new(packet.layer_data.data, 0)?;
let group = TerrainPatchGroupHeader {
stride: bits.unpack_bits(16)?,
patch_size: bits.unpack_bits(8)?,
type_: TerrainPatchLayerType(
u8::try_from(bits.unpack_bits(8)?).map_err(|_| Error::Argument)?,
),
};
if group.patch_size != 16 {
return Err(Error::Argument);
}
match packet.layer_id.type_ {
76 | 77 => self.decode_land(&mut bits, group, simulator, packet.layer_id.type_ == 77),
55 | 57 => Self::decode_wind(&mut bits, group, simulator, packet.layer_id.type_ == 57),
56 | 58 | 87 => Ok(()),
_ => Err(Error::Argument),
}
}
fn decode_land(
&self,
bits: &mut BitPack,
group: TerrainPatchGroupHeader,
simulator: Simulator,
large: bool,
) -> Result<(), Error> {
let mut count = 0usize;
loop {
let header = TerrainCompressor::decode_header(bits, large)?;
if header.quant_w_bits == TerrainCompressor::END_OF_PATCHES {
break;
}
if !large && (!(0..16).contains(&header.x()) || !(0..16).contains(&header.y())) {
return Err(Error::Argument);
}
count += 1;
if count > 4096 {
return Err(Error::Argument);
}
let mut coefficients = vec![0; 1024];
TerrainCompressor::decode_values(&mut coefficients, bits, header, group.patch_size)?;
let heights = TerrainCompressor::decompress_patch(coefficients, header, group)?;
simulator.native_store_terrain_patch(
TerrainPatch {
data: heights.clone(),
x: header.x(),
y: header.y(),
},
large,
)?;
self.events.emit(LandPatchReceivedEventArgs::new(
simulator.clone(),
header.x(),
header.y(),
group.patch_size,
heights,
)?);
}
Ok(())
}
fn decode_wind(
bits: &mut BitPack,
mut group: TerrainPatchGroupHeader,
simulator: Simulator,
large: bool,
) -> Result<(), Error> {
group.stride = group.patch_size;
let mut axes = Vec::with_capacity(2);
for _ in 0..2 {
let header = TerrainCompressor::decode_header(bits, large)?;
if header.quant_w_bits == TerrainCompressor::END_OF_PATCHES {
return Err(Error::Argument);
}
let mut coefficients = vec![0; 1024];
TerrainCompressor::decode_values(&mut coefficients, bits, header, group.patch_size)?;
axes.push(TerrainCompressor::decompress_patch(
coefficients,
header,
group,
)?);
}
simulator.native_store_wind(
axes[0]
.iter()
.zip(&axes[1])
.take(256)
.map(|(x, y)| Vector2 { x: *x, y: *y })
.collect(),
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn land_event_validates_patch_dimensions() {
let client = GridClient::new().unwrap();
let sim = Simulator::new(client, "127.0.0.1:13".parse().unwrap(), 0, None, None).unwrap();
assert!(LandPatchReceivedEventArgs::new(sim, 0, 0, 16, vec![0.0; 255]).is_err());
}
#[test]
fn layer_packet_updates_terrain_before_emitting_the_correlated_patch() {
let client = GridClient::new().unwrap();
let sim = Simulator::new(
client.clone(),
"127.0.0.1:13".parse().unwrap(),
0,
None,
None,
)
.unwrap();
let manager = TerrainManager::new(client).unwrap();
let observed = Arc::new(Mutex::new(None));
let captured = Arc::clone(&observed);
let _subscription = manager.subscribe_land_patch_received(Arc::new(move |event| {
let mut height = 0.0;
let stored = event
.simulator()
.terrain_height_at_point(event.x() * 16, event.y() * 16, &mut height)
.unwrap();
*mutex(&captured) = Some((event.x(), event.y(), stored, height));
}));
let source: Vec<f32> = (0..256).map(|index| 32.0 + index as f32 / 64.0).collect();
let packet = TerrainCompressor::create_land_packet_with_single_array_int32_int32(
source.clone(),
5,
9,
)
.unwrap();
manager.inner.decode_layer(packet, sim).unwrap();
let observed = mutex(&observed).expect("land event");
assert_eq!((observed.0, observed.1, observed.2), (5, 9, true));
assert!((observed.3 - source[0]).abs() < 0.2);
}
#[test]
fn rectangular_large_region_uses_independent_patch_dimensions() {
let client = GridClient::new().unwrap();
let sim = Simulator::new(
client,
"127.0.0.1:13".parse().unwrap(),
0,
Some(512),
Some(256),
)
.unwrap();
sim.native_store_terrain_patch(
TerrainPatch {
data: vec![41.5; 256],
x: 31,
y: 15,
},
true,
)
.unwrap();
let mut height = 0.0;
assert!(sim.terrain_height_at_point(511, 255, &mut height).unwrap());
assert_eq!(height, 41.5);
assert!(
sim.native_store_terrain_patch(
TerrainPatch {
data: vec![0.0; 256],
x: 0,
y: 16,
},
true,
)
.is_err()
);
}
}

73
docs/land.md Normal file
View File

@@ -0,0 +1,73 @@
# Land, terrain, environment, and sound
`GridClient` owns one lazily constructed `ParcelManager`, `TerrainManager`,
`EnvironmentManager`, and `SoundManager`. Cloning a manager keeps the same
registries and state; disposing it releases packet and capability
subscriptions. Incoming state is committed before an event callback is run,
and registries snapshot callbacks before invoking them so callbacks never run
under a manager lock.
## Parcels
Parcel properties received from the capability event queue retain their
request sequence, result, selection state, region-local coordinates, flags,
permissions, media settings, and 512-byte occupancy bitmap. The bitmap is
expanded to the simulator's four-metre parcel map. Overlay packets are four
fixed 1 KiB segments: they may arrive out of order, duplicate sequence IDs do
not advance completion, and the downloaded event is emitted only after all
four unique segments are present.
Requests select the reference LLUDP packet for property, access-list, dwell,
owner, update, subdivision/join, purchase, reclaim, deed, release, auction,
selection/return, and terraform operations. Remote parcel lookup and script
resource summary/detail use their named capabilities. Resource replies,
variable blocks, strings, bitmaps, and HTTP bodies are bounded before state is
published. Bulk parcel discovery is cancellation-aware and skips cells already
covered by received bitmaps.
Parcel and estate mutation methods send real protocol operations. Applications
must place user confirmation and authorization policy in front of destructive
operations. Live-grid tests must remain ignored unless explicit credentials
and opt-in configuration are supplied; the default tests use only synthetic
packets and in-memory HTTP handlers.
## Terrain and wind
`TerrainCompressor` implements the 16×16 DCT patch format, including zig-zag
coefficient order, quantization headers, normal and large-region coordinate
encodings, end markers, and bounded layer construction. `TerrainManager`
decodes land and wind layer packets, stores patches before emitting
`LandPatchReceived`, and keeps width and height independent for rectangular
variable regions. `Simulator::terrain_height_at_point` resolves a local metre
coordinate through the stored patch grid.
PBR terrain material overrides use the `ModifyRegion` capability. Exactly four
slots are accepted. Empty maps remain empty slots; non-empty maps use the
native `AssetMaterial` override codec. Capability and LLSD response bodies are
limited to 16 MiB and operations honor cancellation.
## Environment
Extended environment requests use `ExtEnvironment`; parcel operations append
the compatibility query key `parcelid`, while region operations leave the URI
unchanged. GET, POST, and DELETE implement retrieval, update, and reset.
`EnvironmentSettings` provides the legacy LLSD path. Parsed region and legacy
state is installed before update events, and all request/response bodies are
limited to 16 MiB.
## Sound
The sound manager emits typed events for attached sounds, attached gain
changes, preload queues, and positional sound triggers. UUID identity, gain,
region handle, position, and `SoundFlags` (including queueing) are retained.
Outgoing triggers validate IDs and finite values, clamp gain to the protocol
range, and select the current or explicitly supplied simulator and region
handle.
## Focused validation
The native tests cover terrain golden round trips and bounds, rectangular
region addressing, parcel bitmap and overlay correlation, capability parcel
state publication, environment LLSD shapes, sound packet identity/queueing, and
state-before-callback ordering. The translated compatibility cases cover
extended-environment HTTP selection and all four terrain-material slots.

View File

@@ -81,8 +81,7 @@ fn world_facing_constructors_preserve_implemented_and_pending_behavior() {
assert!(parcel.always_request_acl);
assert!(parcel.always_request_dwell);
assert!(!parcel.pool_parcel_data);
assert_eq!(
member_id(TerrainPatch::new()),
"M:LibreMetaverse.TerrainPatch.#ctor"
);
let terrain = TerrainPatch::new().expect("TerrainPatch constructor");
assert!(terrain.data.is_empty());
assert_eq!((terrain.x, terrain.y), (0, 0));
}

View File

@@ -147,6 +147,36 @@ NATIVE_TYPES = {
"T:LibreMetaverse.RegionExperiencesEventArgs": "crate::agent_messages::RegionExperiencesEventArgs",
"T:LibreMetaverse.SetDisplayNameReplyEventArgs": "crate::agent_messages::SetDisplayNameReplyEventArgs",
"T:LibreMetaverse.TeleportEventArgs": "crate::agent_movement::TeleportEventArgs",
"T:LibreMetaverse.TerrainCompressor": "crate::terrain_codec::TerrainCompressor",
"T:LibreMetaverse.TerrainPatch": "crate::terrain_codec::TerrainPatch",
"T:LibreMetaverse.TerrainPatch.GroupHeader": "crate::terrain_codec::TerrainPatchGroupHeader",
"T:LibreMetaverse.TerrainPatch.Header": "crate::terrain_codec::TerrainPatchHeader",
"T:LibreMetaverse.TerrainManager": "crate::terrain_manager::TerrainManager",
"T:LibreMetaverse.LandPatchReceivedEventArgs": "crate::terrain_manager::LandPatchReceivedEventArgs",
"T:LibreMetaverse.SoundManager": "crate::sound_manager::SoundManager",
"T:LibreMetaverse.AttachedSoundEventArgs": "crate::sound_manager::AttachedSoundEventArgs",
"T:LibreMetaverse.AttachedSoundGainChangeEventArgs": "crate::sound_manager::AttachedSoundGainChangeEventArgs",
"T:LibreMetaverse.PreloadSoundEventArgs": "crate::sound_manager::PreloadSoundEventArgs",
"T:LibreMetaverse.SoundTriggerEventArgs": "crate::sound_manager::SoundTriggerEventArgs",
"T:LibreMetaverse.Parcel": "crate::parcel_manager::Parcel",
"T:LibreMetaverse.ParcelInfo": "crate::parcel_manager::ParcelInfo",
"T:LibreMetaverse.ParcelMedia": "crate::parcel_manager::ParcelMedia",
"T:LibreMetaverse.ParcelManager": "crate::parcel_manager::ParcelManager",
"T:LibreMetaverse.ParcelManager.ParcelAccessEntry": "crate::parcel_manager::ParcelManagerParcelAccessEntry",
"T:LibreMetaverse.ParcelManager.ParcelPrimOwners": "crate::parcel_manager::ParcelManagerParcelPrimOwners",
"T:LibreMetaverse.ForceSelectObjectsReplyEventArgs": "crate::parcel_manager::ForceSelectObjectsReplyEventArgs",
"T:LibreMetaverse.ParcelAccessListReplyEventArgs": "crate::parcel_manager::ParcelAccessListReplyEventArgs",
"T:LibreMetaverse.ParcelDwellReplyEventArgs": "crate::parcel_manager::ParcelDwellReplyEventArgs",
"T:LibreMetaverse.ParcelInfoReplyEventArgs": "crate::parcel_manager::ParcelInfoReplyEventArgs",
"T:LibreMetaverse.ParcelMediaCommandEventArgs": "crate::parcel_manager::ParcelMediaCommandEventArgs",
"T:LibreMetaverse.ParcelMediaUpdateReplyEventArgs": "crate::parcel_manager::ParcelMediaUpdateReplyEventArgs",
"T:LibreMetaverse.ParcelObjectOwnersReplyEventArgs": "crate::parcel_manager::ParcelObjectOwnersReplyEventArgs",
"T:LibreMetaverse.ParcelOverlayEventArgs": "crate::parcel_manager::ParcelOverlayEventArgs",
"T:LibreMetaverse.ParcelPropertiesEventArgs": "crate::parcel_manager::ParcelPropertiesEventArgs",
"T:LibreMetaverse.SimParcelsDownloadedEventArgs": "crate::parcel_manager::SimParcelsDownloadedEventArgs",
"T:LibreMetaverse.Messages.Linden.ObjectResourcesDetail": "crate::parcel_manager::ObjectResourcesDetail",
"T:LibreMetaverse.Messages.Linden.ParcelResourcesDetail": "crate::parcel_manager::ParcelResourcesDetail",
"T:LibreMetaverse.Messages.Linden.LandResourcesInfo": "crate::parcel_manager::LandResourcesInfo",
"T:LibreMetaverse.ProductInfoEventArgs": "crate::agent_messages::ProductInfoEventArgs",
"T:LibreMetaverse.RegionCrossedEventArgs": "crate::agent_movement::RegionCrossedEventArgs",
"T:LibreMetaverse.RegionCrossingPredictionEventArgs": "crate::agent_movement::RegionCrossingPredictionEventArgs",
@@ -177,6 +207,13 @@ NATIVE_TYPES = {
"T:LibreMetaverse.Caps.EventQueueCallback": "crate::network_manager::CapsEventQueueCallback",
"T:LibreMetaverse.CapsEventDictionary": "crate::network_manager::CapsEventDictionary",
"T:LibreMetaverse.DisconnectedEventArgs": "crate::network_manager::DisconnectedEventArgs",
"T:LibreMetaverse.EnvironmentManager": "crate::environment_manager::EnvironmentManager",
"T:LibreMetaverse.LegacyEnvironmentEventArgs": "crate::environment_manager::LegacyEnvironmentEventArgs",
"T:LibreMetaverse.ParcelEnvironmentEventArgs": "crate::environment_manager::ParcelEnvironmentEventArgs",
"T:LibreMetaverse.RegionEnvironmentEventArgs": "crate::environment_manager::RegionEnvironmentEventArgs",
"T:LibreMetaverse.Messages.Linden.EnvironmentData": "crate::environment_manager::EnvironmentData",
"T:LibreMetaverse.Messages.Linden.ExtEnvironmentMessage": "crate::environment_manager::ExtEnvironmentMessage",
"T:LibreMetaverse.Messages.Linden.LegacyEnvironmentMessage": "crate::environment_manager::LegacyEnvironmentMessage",
"T:LibreMetaverse.EventQueueRunningEventArgs": "crate::network_manager::EventQueueRunningEventArgs",
"T:LibreMetaverse.GenericStreamingMessageEventArgs": "crate::network_manager::GenericStreamingMessageEventArgs",
"T:LibreMetaverse.NetworkManager.IncomingPacket": "crate::network_manager::NetworkManagerIncomingPacket",
@@ -358,6 +395,19 @@ NATIVE_DECLARATIONS = {
}
NATIVE_MEMBER_BODIES = {
"P:LibreMetaverse.GridClient.Environment":
"self.native_environment().unwrap_or_else(|_| panic!(\"failed to construct EnvironmentManager\"))",
"P:LibreMetaverse.GridClient.Environment#set": "self.native_set_environment(value)",
"P:LibreMetaverse.GridClient.Terrain": "self.native_terrain().unwrap_or_else(|_| panic!(\"failed to construct TerrainManager\"))",
"P:LibreMetaverse.GridClient.Terrain#set": "self.native_set_terrain(value)",
"P:LibreMetaverse.GridClient.Sound": "self.native_sound().unwrap_or_else(|_| panic!(\"failed to construct SoundManager\"))",
"P:LibreMetaverse.GridClient.Sound#set": "self.native_set_sound(value)",
"P:LibreMetaverse.GridClient.Parcels": "self.native_parcels().unwrap_or_else(|_| panic!(\"failed to construct ParcelManager\"))",
"P:LibreMetaverse.GridClient.Parcels#set": "self.native_set_parcels(value)",
"M:LibreMetaverse.Simulator.IsParcelMapFull": "Ok(self.native_is_parcel_map_full())",
"M:LibreMetaverse.Simulator.TerrainHeightAtPoint(System.Int32,System.Int32,System.Single@)": "self.native_terrain_height_at_point(x, y, height)",
"P:LibreMetaverse.Simulator.ParcelMap": "self.native_parcel_map_snapshot()",
"P:LibreMetaverse.Simulator.Parcels": "std::sync::RwLock::new(self.native_parcels_snapshot())",
"E:LibreMetaverse.ObjectManager.AvatarSitChanged":
"self.native_subscribe_avatar_sit_changed(handler)",
"E:LibreMetaverse.ObjectManager.AvatarUpdate":