Implement object manager vertical slice (#69)
This commit is contained in:
@@ -118,6 +118,7 @@ struct ClientRuntime {
|
||||
caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
|
||||
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>>>,
|
||||
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>>>,
|
||||
@@ -152,6 +153,7 @@ impl ClientRuntime {
|
||||
caps_rate_limiter: Mutex::new(caps_rate_limiter),
|
||||
network_manager: Mutex::new(std::sync::Weak::new()),
|
||||
agent_manager: Mutex::new(std::sync::Weak::new()),
|
||||
object_manager: Mutex::new(None),
|
||||
appearance_manager: Mutex::new(None),
|
||||
avatar_manager: Mutex::new(None),
|
||||
animesh_manager: Mutex::new(None),
|
||||
@@ -238,6 +240,10 @@ impl ClientRuntime {
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take();
|
||||
self.object_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take();
|
||||
self.http_caps_client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
@@ -629,6 +635,55 @@ impl GridClient {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::downgrade(inner);
|
||||
}
|
||||
|
||||
pub(crate) fn cached_object_manager_inner(
|
||||
&self,
|
||||
) -> Option<Arc<crate::object_manager::ObjectManagerInner>> {
|
||||
self.runtime
|
||||
.object_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn cache_object_manager_inner(
|
||||
&self,
|
||||
inner: &Arc<crate::object_manager::ObjectManagerInner>,
|
||||
) {
|
||||
*self
|
||||
.runtime
|
||||
.object_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::clone(inner));
|
||||
}
|
||||
|
||||
pub(crate) fn install_object_manager_inner(
|
||||
&self,
|
||||
candidate: Arc<crate::object_manager::ObjectManagerInner>,
|
||||
) -> Arc<crate::object_manager::ObjectManagerInner> {
|
||||
let mut cached = self
|
||||
.runtime
|
||||
.object_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_objects(&self) -> Result<crate::ObjectManager, crate::Error> {
|
||||
if let Some(inner) = self.cached_object_manager_inner() {
|
||||
return Ok(crate::ObjectManager { inner });
|
||||
}
|
||||
crate::object_manager::ObjectManagerInner::new(Some(Arc::new(self.clone())))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn native_set_objects(&self, value: crate::ObjectManager) {
|
||||
self.cache_object_manager_inner(&value.inner);
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_caps_rate_limiter(
|
||||
&mut self,
|
||||
value: crate::caps_http::CapsRateLimiter,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ mod login;
|
||||
mod message_codec;
|
||||
mod message_decoder;
|
||||
mod network_manager;
|
||||
mod object_manager;
|
||||
mod object_material;
|
||||
mod object_model;
|
||||
#[rustfmt::skip]
|
||||
|
||||
3019
crates/libremetaverse/src/object_manager.rs
Normal file
3019
crates/libremetaverse/src/object_manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,61 @@
|
||||
)]
|
||||
|
||||
use crate::assets::AssetMaterial;
|
||||
use crate::{Error, GridClient, Simulator};
|
||||
use crate::{Error, GridClient, MediaEntry, Simulator};
|
||||
use flate2::Compression;
|
||||
use flate2::read::ZlibDecoder;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::{UUID, compat::CancellationToken};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ObjectMediaEventArgs {
|
||||
face_media: Vec<MediaEntry>,
|
||||
success: bool,
|
||||
version: String,
|
||||
}
|
||||
|
||||
impl ObjectMediaEventArgs {
|
||||
pub fn new(success: bool, version: String, face_media: Vec<MediaEntry>) -> Result<Self, Error> {
|
||||
if face_media.len() > 45 || version.len() > 16 * 1024 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
face_media,
|
||||
success,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn face_media(&self) -> Vec<MediaEntry> {
|
||||
self.face_media.clone()
|
||||
}
|
||||
|
||||
pub fn set_face_media(&mut self, value: Vec<MediaEntry>) {
|
||||
self.face_media = value;
|
||||
}
|
||||
|
||||
pub const fn success(&self) -> bool {
|
||||
self.success
|
||||
}
|
||||
|
||||
pub fn set_success(&mut self, value: bool) {
|
||||
self.success = value;
|
||||
}
|
||||
|
||||
pub fn version(&self) -> String {
|
||||
self.version.clone()
|
||||
}
|
||||
|
||||
pub fn set_version(&mut self, value: String) {
|
||||
self.version = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GLTFMaterialUpdate {
|
||||
asset_id: Option<Option<UUID>>,
|
||||
@@ -57,16 +105,280 @@ impl GLTFMaterialUpdate {
|
||||
}
|
||||
|
||||
pub struct ObjectManager {
|
||||
client: crate::client_core::ClientWeakHandle,
|
||||
pub(crate) inner: Arc<crate::object_manager::ObjectManagerInner>,
|
||||
}
|
||||
impl ObjectManager {
|
||||
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
client: client.ok_or(Error::ArgumentNull)?.native_weak_handle(),
|
||||
})
|
||||
crate::object_manager::ObjectManagerInner::new(client)
|
||||
}
|
||||
fn client(&self) -> Result<GridClient, Error> {
|
||||
self.client.upgrade().ok_or(Error::InvalidOperation)
|
||||
self.inner.client()
|
||||
}
|
||||
|
||||
fn capability(
|
||||
sim: &Simulator,
|
||||
name: &str,
|
||||
) -> Result<Option<libremetaverse_types::compat::Uri>, Error> {
|
||||
sim.native_caps()
|
||||
.map(|caps| caps.capability_uri(name.to_owned()))
|
||||
.transpose()
|
||||
.map(Option::flatten)
|
||||
}
|
||||
|
||||
async fn post_llsd(
|
||||
&self,
|
||||
uri: libremetaverse_types::compat::Uri,
|
||||
body: OSD,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<(bool, Vec<u8>), Error> {
|
||||
let payload = OSDParser::serialize_llsd_xml_bytes(body)?;
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/llsd+xml".into(),
|
||||
payload,
|
||||
cancellation_token.unwrap_or_default(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(((200..300).contains(&response.status_code), bytes))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_navigate_object_media(
|
||||
&self,
|
||||
prim_id: UUID,
|
||||
face: i32,
|
||||
new_url: String,
|
||||
sim: Simulator,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<(), Error> {
|
||||
if !(0..45).contains(&face) || new_url.len() > 16 * 1024 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let Some(uri) = Self::capability(&sim, "ObjectMediaNavigate")? else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
let (success, _) = self
|
||||
.post_llsd(
|
||||
uri,
|
||||
OSD::Map(HashMap::from([
|
||||
("current_url".into(), OSD::String(new_url)),
|
||||
("object_id".into(), OSD::UUID(prim_id)),
|
||||
("texture_index".into(), OSD::Integer(face)),
|
||||
])),
|
||||
cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
if success {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::HttpRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn native_update_object_media(
|
||||
&self,
|
||||
prim_id: UUID,
|
||||
face_media: Vec<MediaEntry>,
|
||||
sim: Simulator,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<(), Error> {
|
||||
if face_media.len() > 45 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let Some(uri) = Self::capability(&sim, "ObjectMedia")? else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
let media = face_media
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
if entry.is_undefined() {
|
||||
Ok(OSD::Undefined)
|
||||
} else {
|
||||
Ok(OSD::Map(entry.get_osd()?.snapshot()))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
let (success, _) = self
|
||||
.post_llsd(
|
||||
uri,
|
||||
OSD::Map(HashMap::from([
|
||||
("object_id".into(), OSD::UUID(prim_id)),
|
||||
("object_media_data".into(), OSD::Array(media)),
|
||||
("verb".into(), OSD::String("UPDATE".into())),
|
||||
])),
|
||||
cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
if !success {
|
||||
return Err(Error::HttpRequest);
|
||||
}
|
||||
if let Some(local_id) = sim
|
||||
.global_to_local_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&prim_id)
|
||||
.copied()
|
||||
{
|
||||
let mut objects = sim
|
||||
.objects_primitives
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(prim) = objects.get_mut(&local_id) {
|
||||
prim.face_media = face_media;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn native_request_object_media(
|
||||
&self,
|
||||
prim_id: UUID,
|
||||
sim: Simulator,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<(bool, String, Option<Vec<MediaEntry>>), Error> {
|
||||
let Some(uri) = Self::capability(&sim, "ObjectMedia")? else {
|
||||
return Ok((false, String::new(), None));
|
||||
};
|
||||
let (success, bytes) = self
|
||||
.post_llsd(
|
||||
uri,
|
||||
OSD::Map(HashMap::from([
|
||||
("object_id".into(), OSD::UUID(prim_id)),
|
||||
("verb".into(), OSD::String("GET".into())),
|
||||
])),
|
||||
cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
if !success || bytes.is_empty() || bytes.len() > 16 * 1024 * 1024 {
|
||||
return Ok((false, String::new(), None));
|
||||
}
|
||||
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
|
||||
return Ok((false, String::new(), None));
|
||||
};
|
||||
let version = map
|
||||
.get("object_media_version")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let media = match map.get("object_media_data") {
|
||||
Some(OSD::Array(values)) if values.len() <= 45 => Some(
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(MediaEntry::from_osd)
|
||||
.collect::<Result<Vec<_>, Error>>()?,
|
||||
),
|
||||
Some(_) => return Err(Error::Argument),
|
||||
None => None,
|
||||
};
|
||||
if let Some(local_id) = sim
|
||||
.global_to_local_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&prim_id)
|
||||
.copied()
|
||||
{
|
||||
let mut objects = sim
|
||||
.objects_primitives
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(prim) = objects.get_mut(&local_id) {
|
||||
prim.media_version.clone_from(&version);
|
||||
prim.face_media = media.clone().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
Ok((true, version, media))
|
||||
}
|
||||
|
||||
fn decode_legacy_materials(
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<Vec<crate::materials::LegacyMaterial>, Error> {
|
||||
if bytes.is_empty() || bytes.len() > 16 * 1024 * 1024 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let compressed = map.get("Zipped").ok_or(Error::Argument)?.as_binary()?;
|
||||
let mut binary = Vec::new();
|
||||
ZlibDecoder::new(compressed.as_slice())
|
||||
.take(16 * 1024 * 1024 + 1)
|
||||
.read_to_end(&mut binary)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
if binary.len() > 16 * 1024 * 1024 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let decoded = OSDParser::deserialize_llsd_binary_with_bytes(binary)?;
|
||||
let values = match decoded {
|
||||
OSD::Array(values) => values,
|
||||
OSD::Map(values) => values
|
||||
.get("material_data")
|
||||
.and_then(|value| match value {
|
||||
OSD::Array(values) => Some(values.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
_ => return Err(Error::Argument),
|
||||
};
|
||||
if values.len() > 65_535 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
values
|
||||
.into_iter()
|
||||
.map(|entry| match entry {
|
||||
OSD::Map(map) => crate::materials::LegacyMaterial::new_with_osd_map(
|
||||
libremetaverse_structured_data::OSDMap::new_with_dictionary(map)?,
|
||||
),
|
||||
_ => Err(Error::Argument),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn native_request_materials(
|
||||
&self,
|
||||
sim: Simulator,
|
||||
materials: Option<Vec<UUID>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<Box<dyn Iterator<Item = crate::materials::LegacyMaterial>>, Error> {
|
||||
let Some(uri) = Self::capability(&sim, "RenderMaterials")? else {
|
||||
return Ok(Box::new(std::iter::empty()));
|
||||
};
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let (response, bytes) = if let Some(materials) = materials {
|
||||
if materials.is_empty() || materials.len() > 65_535 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let binary = OSDParser::serialize_llsd_binary_with_osd(OSD::Array(
|
||||
materials.into_iter().map(OSD::UUID).collect(),
|
||||
))?;
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(&binary).map_err(|_| Error::Argument)?;
|
||||
let zipped = encoder.finish().map_err(|_| Error::Argument)?;
|
||||
let body = OSD::Map(HashMap::from([("Zipped".into(), OSD::Binary(zipped))]));
|
||||
let payload = OSDParser::serialize_llsd_xml_bytes(body)?;
|
||||
self.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/llsd+xml".into(),
|
||||
payload,
|
||||
token,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
self.client()?
|
||||
.native_http_caps_client()
|
||||
.get(uri, token, None)
|
||||
.await?
|
||||
};
|
||||
if !(200..300).contains(&response.status_code) {
|
||||
return Ok(Box::new(std::iter::empty()));
|
||||
}
|
||||
Ok(Box::new(Self::decode_legacy_materials(bytes)?.into_iter()))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_send_material_updates(
|
||||
@@ -190,3 +502,62 @@ impl ObjectManager {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn legacy_material_capability_response_is_decoded() {
|
||||
let id = UUID::random().unwrap();
|
||||
let material = OSD::Map(HashMap::from([
|
||||
("ID".into(), OSD::UUID(id)),
|
||||
(
|
||||
"Material".into(),
|
||||
OSD::Map(HashMap::from([("SpecExp".into(), OSD::Integer(37))])),
|
||||
),
|
||||
]));
|
||||
let binary = OSDParser::serialize_llsd_binary_with_osd(OSD::Array(vec![material])).unwrap();
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(&binary).unwrap();
|
||||
let response = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
|
||||
"Zipped".into(),
|
||||
OSD::Binary(encoder.finish().unwrap()),
|
||||
)])))
|
||||
.unwrap();
|
||||
|
||||
let decoded = ObjectManager::decode_legacy_materials(response).unwrap();
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert_eq!(decoded[0].id(), id);
|
||||
assert_eq!(decoded[0].specular_exponent(), 37);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_material_capability_rejects_invalid_compression() {
|
||||
let response = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
|
||||
"Zipped".into(),
|
||||
OSD::Binary(vec![0xde, 0xad, 0xbe, 0xef]),
|
||||
)])))
|
||||
.unwrap();
|
||||
|
||||
assert!(ObjectManager::decode_legacy_materials(response).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_media_event_args_preserve_response_values() {
|
||||
let mut media = MediaEntry::new().unwrap();
|
||||
media.current_url = "https://example.invalid/media".into();
|
||||
let mut args =
|
||||
ObjectMediaEventArgs::new(true, "x-mv:0000000001".into(), vec![media]).unwrap();
|
||||
|
||||
assert!(args.success());
|
||||
assert_eq!(args.version(), "x-mv:0000000001");
|
||||
assert_eq!(args.face_media().len(), 1);
|
||||
args.set_success(false);
|
||||
args.set_version("x-mv:0000000002".into());
|
||||
args.set_face_media(Vec::new());
|
||||
assert!(!args.success());
|
||||
assert_eq!(args.version(), "x-mv:0000000002");
|
||||
assert!(args.face_media().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3022,7 +3022,7 @@ impl LegacyMaterial {
|
||||
}
|
||||
}
|
||||
|
||||
const fn profile_curve_from_u8(value: u8) -> ProfileCurve {
|
||||
pub(crate) const fn profile_curve_from_u8(value: u8) -> ProfileCurve {
|
||||
match value {
|
||||
1 => ProfileCurve::Square,
|
||||
2 => ProfileCurve::IsoTriangle,
|
||||
@@ -3032,7 +3032,7 @@ const fn profile_curve_from_u8(value: u8) -> ProfileCurve {
|
||||
_ => ProfileCurve::Circle,
|
||||
}
|
||||
}
|
||||
const fn material_from_i32(value: i32) -> Material {
|
||||
pub(crate) const fn material_from_i32(value: i32) -> Material {
|
||||
match value {
|
||||
1 => Material::Metal,
|
||||
2 => Material::Glass,
|
||||
@@ -3044,7 +3044,7 @@ const fn material_from_i32(value: i32) -> Material {
|
||||
_ => Material::Stone,
|
||||
}
|
||||
}
|
||||
const fn pcode_from_i32(value: i32) -> PCode {
|
||||
pub(crate) const fn pcode_from_i32(value: i32) -> PCode {
|
||||
match value {
|
||||
9 => PCode::Prim,
|
||||
47 => PCode::Avatar,
|
||||
@@ -3055,7 +3055,7 @@ const fn pcode_from_i32(value: i32) -> PCode {
|
||||
_ => PCode::None,
|
||||
}
|
||||
}
|
||||
const fn path_curve_from_i32(value: i32) -> PathCurve {
|
||||
pub(crate) const fn path_curve_from_i32(value: i32) -> PathCurve {
|
||||
match value {
|
||||
32 => PathCurve::Circle,
|
||||
48 => PathCurve::Circle2,
|
||||
|
||||
Reference in New Issue
Block a user