Implement land, terrain, environment, and sound managers (#70)
This commit is contained in:
10
README.md
10
README.md
@@ -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).
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,
|
||||
|
||||
651
crates/libremetaverse/src/environment_manager.rs
Normal file
651
crates/libremetaverse/src/environment_manager.rs
Normal 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
@@ -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.
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
1830
crates/libremetaverse/src/parcel_manager.rs
Normal file
1830
crates/libremetaverse/src/parcel_manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
497
crates/libremetaverse/src/sound_manager.rs
Normal file
497
crates/libremetaverse/src/sound_manager.rs
Normal 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)))
|
||||
);
|
||||
}
|
||||
}
|
||||
578
crates/libremetaverse/src/terrain_codec.rs
Normal file
578
crates/libremetaverse/src/terrain_codec.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
464
crates/libremetaverse/src/terrain_manager.rs
Normal file
464
crates/libremetaverse/src/terrain_manager.rs
Normal 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
73
docs/land.md
Normal 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.
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user