From b1f60db3d60b9942f84a4b3431022034e4bc445b Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Mon, 10 Aug 2026 10:33:29 +0000 Subject: [PATCH] Implement appearance baking pipeline (#66) --- .gitea/workflows/codegen.yml | 4 + README.md | 11 + api/SHIM-COVERAGE.md | 2 +- crates/libremetaverse/src/appearance_baker.rs | 605 ++++++ .../src/appearance_baker_semantics.rs | 23 +- .../libremetaverse/src/appearance_manager.rs | 1631 ++++++++++++++++- crates/libremetaverse/src/asset_models.rs | 145 ++ .../src/baking_texture_provider.rs | 56 + crates/libremetaverse/src/generated.rs | 292 +-- crates/libremetaverse/src/initial_outfit.rs | 607 ++++++ crates/libremetaverse/src/lib.rs | 13 +- docs/appearance.md | 71 +- tests/PARITY.md | 8 +- tests/upstream-tests.json | 12 +- tools/check_milestone_09_issue_66.py | 104 ++ tools/generate_api_shims.py | 149 +- tools/generate_rust_mapping.py | 3 + 17 files changed, 3487 insertions(+), 249 deletions(-) create mode 100644 crates/libremetaverse/src/appearance_baker.rs create mode 100644 crates/libremetaverse/src/baking_texture_provider.rs create mode 100644 crates/libremetaverse/src/initial_outfit.rs create mode 100644 tools/check_milestone_09_issue_66.py diff --git a/.gitea/workflows/codegen.yml b/.gitea/workflows/codegen.yml index 27ad591..6387b9f 100644 --- a/.gitea/workflows/codegen.yml +++ b/.gitea/workflows/codegen.yml @@ -108,6 +108,7 @@ jobs: python3 tools/check_milestone_09_issue_61.py python3 tools/check_milestone_09_issue_62.py python3 tools/check_milestone_09_issue_63.py + python3 tools/check_milestone_09_issue_66.py cargo check --manifest-path tests/api-compile/Cargo.toml --locked - name: Test generated wire codecs and translated parity cases run: | @@ -125,6 +126,9 @@ jobs: run: | cargo test -p libremetaverse --test generated_catalogs cargo test -p libremetaverse-compat-tests --test appearance_visual_semantics + cargo test -p libremetaverse-compat-tests --test appearance_semantics + cargo test -p libremetaverse --lib appearance_ + cargo test -p libremetaverse --lib initial_outfit::tests - name: Test generated skeleton, attention, and genepool catalogs run: | cargo test -p libremetaverse --test generated_avatar_catalogs diff --git a/README.md b/README.md index a1a1bd9..f62f3f1 100644 --- a/README.md +++ b/README.md @@ -392,3 +392,14 @@ link or attachment replacement without publishing partial local state. Composite policies make pure decisions from snapshots and receive committed changes after synchronization is released. The behavior and transaction boundaries are documented in [`docs/appearance.md`](docs/appearance.md). + +The appearance service now completes cached, local, and server-side baking for +all 11 bake targets. It parses wearable parameters and texture indices, +downloads and decodes texture inputs through a bounded provider abstraction, +composites color/alpha/bump layers, uploads JPEG2000 bakes, builds the complete +appearance packet, and handles cache and forced-rebake messages without locks +crossing awaits or callbacks. First-login outfit setup performs a bounded +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). diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 1b5558c..0416143 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.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: 200 types / 15,251 members; remaining surface is callable failure-only shims | +| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 207 types / 15,315 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 | diff --git a/crates/libremetaverse/src/appearance_baker.rs b/crates/libremetaverse/src/appearance_baker.rs new file mode 100644 index 0000000..4335a7e --- /dev/null +++ b/crates/libremetaverse/src/appearance_baker.rs @@ -0,0 +1,605 @@ +//! Bounded, platform-neutral avatar texture compositing. + +use crate::targa::Targa; +use crate::{AppearanceManagerTextureData, AvatarTextureIndex, BakeType, Error}; +use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels as Channels}; +use libremetaverse_types::Color4; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +const CLASSIC_BAKE_SIZE: i32 = 1024; +const EYE_BAKE_SIZE: i32 = 128; + +fn cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// A compositor owns its input images so callers can freely reuse or mutate +/// their originals after adding a layer. +pub struct Baker { + bake_type: BakeType, + bake_width: i32, + bake_height: i32, + textures: Vec, + baked_texture: Option, +} + +impl Baker { + #[allow(clippy::unnecessary_wraps)] // The generated constructor uses the crate Result model. + pub(crate) fn native_new(bake_type: BakeType) -> Result { + let size = if bake_type == BakeType::Eyes { + EYE_BAKE_SIZE + } else { + CLASSIC_BAKE_SIZE + }; + Ok(Self { + bake_type, + bake_width: size, + bake_height: size, + textures: Vec::new(), + baked_texture: None, + }) + } + + pub(crate) fn native_add_texture( + &mut self, + texture: AppearanceManagerTextureData, + ) -> Result<(), Error> { + if self.textures.len() >= 60 { + return Err(Error::Argument); + } + self.textures.push(texture); + Ok(()) + } + + pub(crate) fn native_bake(&mut self) -> Result<(), Error> { + let channels = Channels::COLOR | Channels::ALPHA | Channels::BUMP; + let mut output = ManagedImage::new(self.bake_width, self.bake_height, channels)?; + if self.bake_type == BakeType::Eyes { + fill_dithered(&mut output, Color4::white()); + } else if let Some(first) = self.textures.first() { + fill_dithered(&mut output, first.color); + } + + let has_skin = self.textures.iter().any(|layer| { + layer.texture.is_some() + && matches!( + layer.texture_index, + AvatarTextureIndex::HeadBodypaint + | AvatarTextureIndex::UpperBodypaint + | AvatarTextureIndex::LowerBodypaint + ) + }); + + if self.bake_type == BakeType::Head { + if let Some(layer) = Self::native_load_resource_layer("head_color.tga".to_owned())? { + draw_layer(&mut output, layer, false)?; + } + if let Some(mask) = Self::native_load_resource_layer("head_alpha.tga".to_owned())? { + apply_min_alpha_resized(&mut output, mask)?; + } + if let Some(mask) = Self::native_load_resource_layer("head_skingrain.tga".to_owned())? { + multiply_color_mask(&mut output, mask)?; + } + } else if !has_skin && matches!(self.bake_type, BakeType::UpperBody | BakeType::LowerBody) { + let name = if self.bake_type == BakeType::UpperBody { + "upperbody_color.tga" + } else { + "lowerbody_color.tga" + }; + if let Some(layer) = Self::native_load_resource_layer(name.to_owned())? { + draw_layer(&mut output, layer, false)?; + } + } + + let mut alpha_wearables = Vec::new(); + let mut deferred_head_skin = None; + let mut deferred_head_tattoos = Vec::new(); + for (position, layer) in self.textures.iter().enumerate() { + let Some(asset) = &layer.texture else { + continue; + }; + let Some(source) = &asset.image else { continue }; + let index = layer.texture_index as i32; + if (AvatarTextureIndex::LowerAlpha as i32..=AvatarTextureIndex::HairAlpha as i32) + .contains(&index) + { + if source.channels.contains(Channels::ALPHA) { + alpha_wearables.push(source.clone()?); + } + continue; + } + if self.bake_type == BakeType::Head + && matches!( + layer.texture_index, + AvatarTextureIndex::HeadBodypaint + | AvatarTextureIndex::HeadTattoo + | AvatarTextureIndex::HeadUniversalTattoo + ) + { + if layer.texture_index == AvatarTextureIndex::HeadBodypaint { + deferred_head_skin = Some(layer); + } else { + deferred_head_tattoos.push(layer); + } + continue; + } + composite_texture( + &mut output, + layer, + self.bake_width, + self.bake_height, + self.bake_type, + has_skin, + position == 0 && matches!(self.bake_type, BakeType::Skirt | BakeType::Hair), + )?; + } + if let Some(layer) = deferred_head_skin { + draw_head_layer(&mut output, layer, self.bake_width, self.bake_height)?; + } + for layer in deferred_head_tattoos { + draw_head_layer(&mut output, layer, self.bake_width, self.bake_height)?; + } + for mut mask in alpha_wearables { + mask.resize_nearest_neighbor(self.bake_width, self.bake_height)?; + apply_min_alpha(&mut output, &mask); + } + + let texture = crate::assets::AssetTexture::new_with_managed_image(output)?; + // JPEG2000 is the protocol representation. Keep the decoded image even + // when a build intentionally excludes that optional codec. + #[cfg(feature = "jpeg2000")] + texture.encode()?; + self.baked_texture = Some(texture); + Ok(()) + } + + pub(crate) fn native_bake_type_for(index: AvatarTextureIndex) -> BakeType { + match index { + AvatarTextureIndex::HeadBodypaint => BakeType::Head, + AvatarTextureIndex::UpperBodypaint + | AvatarTextureIndex::UpperGloves + | AvatarTextureIndex::UpperUndershirt + | AvatarTextureIndex::UpperShirt + | AvatarTextureIndex::UpperJacket => BakeType::UpperBody, + AvatarTextureIndex::LowerBodypaint + | AvatarTextureIndex::LowerUnderpants + | AvatarTextureIndex::LowerSocks + | AvatarTextureIndex::LowerShoes + | AvatarTextureIndex::LowerPants + | AvatarTextureIndex::LowerJacket => BakeType::LowerBody, + AvatarTextureIndex::EyesIris => BakeType::Eyes, + AvatarTextureIndex::Skirt => BakeType::Skirt, + AvatarTextureIndex::Hair => BakeType::Hair, + _ => BakeType::Unknown, + } + } + + pub(crate) fn native_load_resource_layer( + file_name: String, + ) -> Result, Error> { + let path = Path::new(&file_name); + let supported = path.extension().is_some_and(|extension| { + extension.eq_ignore_ascii_case("tga") || extension.eq_ignore_ascii_case("dds") + }); + if file_name.is_empty() || path.components().count() != 1 || !supported { + return Ok(None); + } + if let Some(image) = cache() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .get(&file_name) + { + return image.clone().map(Some); + } + let image = resource_candidates(&file_name) + .into_iter() + .find_map(|path| { + Targa::decode_to_managed_image_with_string(path.to_string_lossy().into_owned()).ok() + }) + .or_else(|| bundled_fallback(&file_name).ok().flatten()); + if let Some(image) = image { + let result = image.clone()?; + cache() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(file_name, image); + Ok(Some(result)) + } else { + Ok(None) + } + } + + pub(crate) fn native_resize_to_bake_dimensions( + texture: ManagedImage, + width: i32, + height: i32, + ) -> Result, Error> { + if texture.width == width && texture.height == height { + return Ok(Some(texture)); + } + if texture.width < width && texture.height < height { + return tile(&texture, width, height).map(Some); + } + let mut texture = texture; + texture.resize_nearest_neighbor(width, height)?; + Ok(Some(texture)) + } + + pub(crate) const fn native_bake_height(&self) -> i32 { + self.bake_height + } + pub(crate) const fn native_bake_width(&self) -> i32 { + self.bake_width + } + pub(crate) const fn native_bake_type(&self) -> BakeType { + self.bake_type + } + pub(crate) fn native_baked_texture(&self) -> Option { + self.baked_texture + .as_ref() + .and_then(|texture| texture.native_deep_clone().ok()) + } + pub(crate) fn native_textures(&self) -> Vec { + self.textures + .iter() + .filter_map(clone_texture_data) + .collect() + } +} + +fn clone_texture_data( + layer: &AppearanceManagerTextureData, +) -> Option { + Some(AppearanceManagerTextureData { + alpha_masks: layer.alpha_masks.clone(), + color: layer.color, + texture: layer + .texture + .as_ref() + .map(crate::assets::AssetTexture::native_deep_clone) + .transpose() + .ok()?, + texture_id: layer.texture_id, + texture_index: layer.texture_index, + }) +} + +fn resource_candidates(name: &str) -> Vec { + let mut roots = Vec::new(); + if let Some(root) = std::env::var_os("LIBREMETAVERSE_RESOURCE_DIR") { + roots.push(PathBuf::from(root)); + } + roots.push(PathBuf::from(crate::Settings::resource_dir())); + roots.push(PathBuf::from("crates/libremetaverse/linden")); + roots + .into_iter() + .map(|root| root.join("character").join(name)) + .collect() +} + +fn bundled_fallback(name: &str) -> Result, Error> { + // The canonical character XML is embedded in generated Rust. These neutral + // base masks keep head/upper/lower baking functional in minimal deployments + // that deliberately omit the optional viewer resource bundle. + let (channels, value) = match name { + "head_color.tga" | "upperbody_color.tga" | "lowerbody_color.tga" => (Channels::COLOR, 255), + "head_alpha.tga" | "head_skingrain.tga" | "head_hair.tga" => (Channels::GRAY, 255), + _ => return Ok(None), + }; + let mut image = ManagedImage::new(512, 512, channels)?; + image.red.fill(value); + image.green.fill(value); + image.blue.fill(value); + Ok(Some(image)) +} + +fn tile(source: &ManagedImage, width: i32, height: i32) -> Result { + source.validate()?; + let mut output = ManagedImage::new(width, height, source.channels)?; + let sw = usize::try_from(source.width).map_err(|_| Error::Argument)?; + let sh = usize::try_from(source.height).map_err(|_| Error::Argument)?; + let dw = usize::try_from(width).map_err(|_| Error::Argument)?; + let dh = usize::try_from(height).map_err(|_| Error::Argument)?; + for y in 0..dh { + for x in 0..dw { + let src = (y % sh) * sw + x % sw; + let dst = y * dw + x; + for (target, input) in [ + (&mut output.red, &source.red), + (&mut output.green, &source.green), + (&mut output.blue, &source.blue), + (&mut output.alpha, &source.alpha), + (&mut output.bump, &source.bump), + ] { + if !input.is_empty() { + target[dst] = input[src]; + } + } + } + } + Ok(output) +} + +fn composite_texture( + output: &mut ManagedImage, + layer: &AppearanceManagerTextureData, + width: i32, + height: i32, + bake_type: BakeType, + has_skin: bool, + preserve_alpha: bool, +) -> Result<(), Error> { + let source = layer + .texture + .as_ref() + .and_then(|asset| asset.image.as_ref()) + .ok_or(Error::InvalidOperation)? + .clone()?; + let mut source = Baker::native_resize_to_bake_dimensions(source, width, height)? + .ok_or(Error::InvalidOperation)?; + let skin = matches!( + layer.texture_index, + AvatarTextureIndex::HeadBodypaint + | AvatarTextureIndex::UpperBodypaint + | AvatarTextureIndex::LowerBodypaint + ); + if !has_skin && bake_type == BakeType::Head && layer.texture_index == AvatarTextureIndex::Hair { + if source.channels.contains(Channels::ALPHA) { + source.alpha.fill(255); + } + if let Some(mask) = Baker::native_load_resource_layer("head_hair.tga".to_owned())? { + multiply_color_mask(&mut source, mask)?; + } + } + if !skin { + tint(&mut source, layer.color); + } + if bake_type == BakeType::Hair { + if source.channels.contains(Channels::ALPHA) { + output.bump.clone_from(&source.alpha); + } else { + output.bump.fill(255); + } + } + if !skin && bake_type != BakeType::Hair && !layer.alpha_masks.is_empty() { + let mut combined = ManagedImage::new(width, height, Channels::ALPHA)?; + let mut normal_count = 0; + for (param, value) in &layer.alpha_masks { + if !param.multiply_blend + && mask_belongs(bake_type, ¶m.tga_file) + && (*value > 0.0 || !param.skip_if_zero) + { + apply_parameter_mask(&mut combined, param, *value)?; + normal_count += 1; + } + } + if normal_count == 0 { + combined.alpha.fill(255); + } + for (param, value) in &layer.alpha_masks { + if param.multiply_blend + && mask_belongs(bake_type, ¶m.tga_file) + && (*value > 0.0 || !param.skip_if_zero) + { + apply_parameter_mask(&mut combined, param, *value)?; + } + } + if !source.channels.contains(Channels::ALPHA) { + source.convert_channels(source.channels | Channels::ALPHA)?; + } + apply_min_alpha(&mut source, &combined); + if layer.texture_index == crate::appearance_manager::morph_layer_for_bake_type(bake_type) { + output.bump.clone_from(&source.alpha); + } + } + draw_layer(output, source, preserve_alpha) +} + +fn draw_head_layer( + output: &mut ManagedImage, + layer: &AppearanceManagerTextureData, + width: i32, + height: i32, +) -> Result<(), Error> { + let source = layer + .texture + .as_ref() + .and_then(|asset| asset.image.as_ref()) + .ok_or(Error::InvalidOperation)? + .clone()?; + let source = Baker::native_resize_to_bake_dimensions(source, width, height)? + .ok_or(Error::InvalidOperation)?; + draw_layer(output, source, false) +} + +fn mask_belongs(bake: BakeType, name: &str) -> bool { + (bake != BakeType::LowerBody || (!name.contains("upper") && !name.contains("shirt"))) + && (bake != BakeType::UpperBody || !name.contains("lower")) +} + +fn effective_mask(image: &ManagedImage) -> Vec { + if image.channels.contains(Channels::ALPHA) { + return image.alpha.clone(); + } + if image.channels.contains(Channels::GRAY) { + return image.red.clone(); + } + if image.channels.contains(Channels::COLOR) { + return image + .red + .iter() + .zip(&image.green) + .zip(&image.blue) + .map(|((&red, &green), &blue)| { + u8::try_from((u16::from(red) + u16::from(green) + u16::from(blue)) / 3) + .unwrap_or(255) + }) + .collect(); + } + let length = image + .width + .checked_mul(image.height) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0); + vec![255; length] +} + +fn apply_parameter_mask( + destination: &mut ManagedImage, + param: &crate::VisualAlphaParam, + value: f32, +) -> Result<(), Error> { + let Some(mut source) = Baker::native_load_resource_layer(param.tga_file.clone())? else { + return Ok(()); + }; + source.resize_nearest_neighbor(destination.width, destination.height)?; + let mask = effective_mask(&source); + let threshold = quantize_u8(1.0 - value.clamp(0.0, 1.0)); + for (target, sample) in destination.alpha.iter_mut().zip(mask) { + let alpha = if sample <= threshold { 0 } else { 255 }; + if param.multiply_blend { + *target = ((u16::from(*target) * u16::from(alpha)) >> 8) as u8; + } else { + *target = (*target).max(alpha); + } + } + Ok(()) +} + +fn apply_min_alpha_resized( + destination: &mut ManagedImage, + mut source: ManagedImage, +) -> Result<(), Error> { + source.resize_nearest_neighbor(destination.width, destination.height)?; + let mask = effective_mask(&source); + for (target, sample) in destination.alpha.iter_mut().zip(mask) { + *target = (*target).min(sample); + } + Ok(()) +} + +fn multiply_color_mask( + destination: &mut ManagedImage, + mut source: ManagedImage, +) -> Result<(), Error> { + source.resize_nearest_neighbor(destination.width, destination.height)?; + let mask = effective_mask(&source); + if !destination.channels.contains(Channels::COLOR) { + return Ok(()); + } + for (index, sample) in mask.into_iter().enumerate() { + destination.red[index] = + ((u16::from(destination.red[index]) * u16::from(sample)) >> 8) as u8; + destination.green[index] = + ((u16::from(destination.green[index]) * u16::from(sample)) >> 8) as u8; + destination.blue[index] = + ((u16::from(destination.blue[index]) * u16::from(sample)) >> 8) as u8; + } + Ok(()) +} + +fn fill_dithered(image: &mut ManagedImage, color: Color4) { + let (red, green, blue) = ( + quantize_u8(color.r), + quantize_u8(color.g), + quantize_u8(color.b), + ); + let alt = |value: u8| if value == 255 { 254 } else { value + 1 }; + let width = usize::try_from(image.width).unwrap_or(1); + for (index, target_red) in image.red.iter_mut().enumerate() { + let column = index % width; + let row = index / width; + let alternate = ((column ^ row) & 0x10) == 0; + *target_red = if alternate { alt(red) } else { red }; + image.green[index] = if alternate { green } else { alt(green) }; + image.blue[index] = if alternate { blue } else { alt(blue) }; + } + image.alpha.fill(255); + image.bump.fill(0); +} + +fn tint(image: &mut ManagedImage, color: Color4) { + if image.channels.contains(Channels::GRAY) && !image.channels.contains(Channels::COLOR) { + let gray = image.red.clone(); + image.channels = Channels(image.channels.0 & !Channels::GRAY.0) | Channels::COLOR; + image.red.clone_from(&gray); + image.green.clone_from(&gray); + image.blue = gray; + } + if !image.channels.contains(Channels::COLOR) { + return; + } + let (red, green, blue) = ( + u16::from(quantize_u8(color.r)), + u16::from(quantize_u8(color.g)), + u16::from(quantize_u8(color.b)), + ); + for index in 0..image.red.len() { + image.red[index] = ((u16::from(image.red[index]) * red) >> 8) as u8; + image.green[index] = ((u16::from(image.green[index]) * green) >> 8) as u8; + image.blue[index] = ((u16::from(image.blue[index]) * blue) >> 8) as u8; + } +} + +#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +fn quantize_u8(value: f32) -> u8 { + (value.clamp(0.0, 1.0) * 255.0).round() as u8 +} + +fn draw_layer( + output: &mut ManagedImage, + mut source: ManagedImage, + add_source_alpha: bool, +) -> Result<(), Error> { + if source.width != output.width || source.height != output.height { + source.resize_nearest_neighbor(output.width, output.height)?; + } + let color = source.channels.contains(Channels::COLOR); + let gray = source.channels.contains(Channels::GRAY); + let alpha = source.channels.contains(Channels::ALPHA); + for index in 0..output.red.len() { + let source_alpha = if alpha { source.alpha[index] } else { 255 }; + let inverse_alpha = 255_u16 - u16::from(source_alpha); + if color || gray { + let sr = source.red[index]; + let sg = if color { source.green[index] } else { sr }; + let sb = if color { source.blue[index] } else { sr }; + if alpha { + output.red[index] = ((u16::from(output.red[index]) * inverse_alpha + + u16::from(sr) * u16::from(source_alpha)) + >> 8) as u8; + output.green[index] = ((u16::from(output.green[index]) * inverse_alpha + + u16::from(sg) * u16::from(source_alpha)) + >> 8) as u8; + output.blue[index] = ((u16::from(output.blue[index]) * inverse_alpha + + u16::from(sb) * u16::from(source_alpha)) + >> 8) as u8; + } else { + output.red[index] = sr; + output.green[index] = sg; + output.blue[index] = sb; + } + } + if add_source_alpha && alpha { + output.alpha[index] = output.alpha[index].min(source_alpha); + } + if source.channels.contains(Channels::BUMP) { + output.bump[index] = source.bump[index]; + } + } + Ok(()) +} + +fn apply_min_alpha(output: &mut ManagedImage, mask: &ManagedImage) { + let effective = if mask.channels.contains(Channels::ALPHA) { + &mask.alpha + } else { + &mask.red + }; + for (target, source) in output.alpha.iter_mut().zip(effective) { + *target = (*target).min(*source); + } +} diff --git a/crates/libremetaverse/src/appearance_baker_semantics.rs b/crates/libremetaverse/src/appearance_baker_semantics.rs index 6997d4c..7f1d08f 100644 --- a/crates/libremetaverse/src/appearance_baker_semantics.rs +++ b/crates/libremetaverse/src/appearance_baker_semantics.rs @@ -35,7 +35,12 @@ fn bake_grayscale_layer_texture_does_not_throw() { let mut baker = imaging::Baker::new(BakeType::UpperBody).unwrap(); baker.add_texture(data).unwrap(); baker.bake().unwrap(); - assert!(baker.baked_texture().is_some()); + let baked = baker.baked_texture().unwrap(); + let image = baked.image.unwrap(); + assert_eq!((image.width, image.height), (1024, 1024)); + assert_eq!((image.red[0], image.green[0], image.blue[0]), (63, 31, 94)); + assert_eq!(image.alpha[0], 255); + assert_eq!(image.bump[0], 0); } // parity-case: LibreMetaverse.Tests/BakerTests.cs::BakerTests.Bake_GrayscaleHairTextureNoSkin_DoesNotThrow::test e48b737d9138e63cda98b5e80bf561a80d1dab5de78901ac9e1bb320ca281eb6 translated @@ -97,3 +102,19 @@ fn resize_to_bake_dimensions_source_smaller_in_one_axis_only_scales_rather_than_ assert_eq!(result.height, 4); assert!(result.red[3 * 4] > 50); } + +#[test] +fn bake_type_for_preserves_the_reference_base_layer_mapping() { + assert_eq!( + imaging::Baker::bake_type_for(AvatarTextureIndex::UpperShirt).unwrap(), + BakeType::UpperBody + ); + assert_eq!( + imaging::Baker::bake_type_for(AvatarTextureIndex::UpperTattoo).unwrap(), + BakeType::Unknown + ); + assert_eq!( + imaging::Baker::bake_type_for(AvatarTextureIndex::Aux1Tattoo).unwrap(), + BakeType::Unknown + ); +} diff --git a/crates/libremetaverse/src/appearance_manager.rs b/crates/libremetaverse/src/appearance_manager.rs index 038b7b2..a427558 100644 --- a/crates/libremetaverse/src/appearance_manager.rs +++ b/crates/libremetaverse/src/appearance_manager.rs @@ -3,28 +3,38 @@ use crate::client_core::ClientWeakHandle; use crate::packet_catalog::PacketType; use crate::packets::{ - AgentIsNowWearingPacket, AgentIsNowWearingPacketWearableDataBlock, - DetachAttachmentIntoInvPacket, RezSingleAttachmentFromInvPacket, + AgentCachedTexturePacket, AgentCachedTexturePacketWearableDataBlock, + AgentCachedTextureResponsePacket, AgentIsNowWearingPacket, + AgentIsNowWearingPacketWearableDataBlock, AgentSetAppearancePacket, + AgentSetAppearancePacketVisualParamBlock, AgentSetAppearancePacketWearableDataBlock, + AgentWearablesRequestPacket, AgentWearablesUpdatePacket, DetachAttachmentIntoInvPacket, + RebakeAvatarTexturesPacket, RezSingleAttachmentFromInvPacket, }; use crate::{ - AgentWearablesReplyEventArgs, AppearanceManagerWearableData, AppearanceSetEventArgs, - GridClient, InventoryBase, InventoryFolder, InventoryItem, InventoryObject, - InventoryObjectClass, InventoryWearable, Permissions, PrimitiveTextureEntry, - appearance::CurrentOutfitFolder, + AgentCachedBakesReplyEventArgs, AgentWearablesReplyEventArgs, AppearanceManagerTextureData, + AppearanceManagerWearableData, AvatarTextureIndex, BakeType, GridClient, InventoryAttachment, + InventoryBase, InventoryFolder, InventoryItem, InventoryObject, InventoryObjectClass, + InventorySortOrder, InventoryWearable, Permissions, PrimitiveTextureEntry, VisualParams, +}; +use futures_util::{StreamExt, stream::FuturesUnordered}; +use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser}; +use libremetaverse_types::compat::{ + CancellationToken, CancellationTokenSource, EventHandler, Subscription, }; -use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription}; use libremetaverse_types::{ - AssetType, AttachmentPoint, Error, FolderType, InventoryType, MultiValueDictionary, UUID, - WearableType, + AssetType, AttachmentPoint, Color4, Error, FolderType, InventoryType, MultiValueDictionary, + UUID, Vector3, WearableType, }; use std::collections::{HashMap, HashSet, VecDeque}; use std::fmt; +use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -const ATTACHMENT_ADD: u8 = 0x80; +pub(crate) const ATTACHMENT_ADD: u8 = 0x80; const MAX_WEARABLE_LAYERS: usize = 60; const MAX_FOLDER_TRAVERSAL: usize = 256; +const MAX_COF_ENTRIES: usize = 512; fn mutex(lock: &Mutex) -> std::sync::MutexGuard<'_, T> { lock.lock() @@ -122,6 +132,8 @@ struct AttachmentRecord { struct AppearanceEvents { agent_wearables_reply: EventRegistry, appearance_set: EventRegistry, + cached_bakes_reply: EventRegistry, + rebake_avatar_requested: EventRegistry, } pub(crate) struct AppearanceManagerInner { @@ -131,7 +143,21 @@ pub(crate) struct AppearanceManagerInner { last_cof_version: AtomicI32, busy: AtomicBool, disposed: AtomicBool, + texture_provider: RwLock>, + texture_slots: RwLock>, + wearable_assets: RwLock>, + visual_parameters: RwLock>, + server_visual_parameters: AtomicBool, + cache_serial: AtomicI32, + cache_reply_generation: AtomicU64, + cache_notify: tokio::sync::Notify, + wearables_reply_generation: AtomicU64, + wearables_notify: tokio::sync::Notify, + appearance_serial: AtomicU64, + appearance_cancel: Mutex>, + appearance_gate: Arc, events: AppearanceEvents, + network_subscriptions: Mutex>, } /// Shared native manager. Public texture fields remain on the compatibility @@ -142,6 +168,35 @@ pub struct AppearanceManager { inner: Arc, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AppearanceSetEventArgs { + success: bool, +} + +impl AppearanceSetEventArgs { + pub(crate) const fn native_new(success: bool) -> Self { + Self { success } + } + #[allow(clippy::trivially_copy_pass_by_ref)] // Generated property access uses `&self`. + pub(crate) const fn native_success(&self) -> bool { + self.success + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RebakeAvatarTexturesEventArgs { + texture_id: UUID, +} + +impl RebakeAvatarTexturesEventArgs { + pub(crate) const fn native_new(texture_id: UUID) -> Self { + Self { texture_id } + } + pub(crate) const fn native_texture_id(&self) -> UUID { + self.texture_id + } +} + impl Clone for AppearanceManager { fn clone(&self) -> Self { Self::native_from_inner(Arc::clone(&self.inner)) @@ -167,27 +222,176 @@ impl Drop for BusyGuard<'_> { } } +fn handle_appearance_packet( + inner: &Arc, + packet_type: PacketType, + data: Vec, +) { + match packet_type { + PacketType::AgentWearablesUpdate => handle_wearables_update(inner, data), + PacketType::AgentCachedTextureResponse => handle_cached_bakes(inner, data), + PacketType::RebakeAvatarTextures => handle_rebake_request(inner, data), + _ => {} + } +} + +fn handle_wearables_update(inner: &Arc, data: Vec) { + let mut offset = 0; + let Ok(packet) = AgentWearablesUpdatePacket::new_with_bytes_int32(data, &mut offset) else { + return; + }; + let server_baking = inner + .client + .upgrade() + .and_then(|client| client.native_network().ok()) + .and_then(|network| network.native_current_sim()) + .is_some_and(|simulator| { + simulator.protocols.0 & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0 + }); + let mut received = Vec::new(); + if !server_baking { + for block in packet.wearable_data { + let Some(wearable_type) = wearable_type_from_u8(block.wearable_type) else { + continue; + }; + if wearable_type == WearableType::Invalid || block.item_id == UUID::zero() { + continue; + } + received.push(WearableRecord { + item_id: block.item_id, + asset_id: block.asset_id, + asset_type: wearable_type_to_asset_type(wearable_type), + wearable_type, + }); + } + *write(&inner.wearables) = received; + inner + .server_visual_parameters + .store(false, Ordering::Release); + } + inner + .wearables_reply_generation + .fetch_add(1, Ordering::AcqRel); + inner.wearables_notify.notify_one(); + inner + .events + .agent_wearables_reply + .emit_with(|| AgentWearablesReplyEventArgs); + if !server_baking && !read(&inner.wearables).is_empty() && !inner.busy.load(Ordering::Acquire) { + spawn_appearance_request(inner, false, "appearance-wearables"); + } +} + +fn handle_cached_bakes(inner: &Arc, data: Vec) { + let mut offset = 0; + let Ok(packet) = AgentCachedTextureResponsePacket::new_with_bytes_int32(data, &mut offset) + else { + return; + }; + let mut slots = write(&inner.texture_slots); + for bake in packet.wearable_data { + if let Some(target) = bake_index_to_texture_index() + .get(usize::from(bake.texture_index)) + .copied() + && let Some(slot) = slots.get_mut(usize::from(target)) + && bake.texture_id != UUID::zero() + { + slot.texture_id = bake.texture_id; + slot.texture_index = avatar_texture_index_from_i32(i32::from(target)) + .unwrap_or(AvatarTextureIndex::Unknown); + } + } + drop(slots); + inner.cache_reply_generation.fetch_add(1, Ordering::AcqRel); + inner.cache_notify.notify_one(); + inner + .events + .cached_bakes_reply + .emit_with(|| AgentCachedBakesReplyEventArgs); +} + +fn handle_rebake_request(inner: &Arc, data: Vec) { + let mut offset = 0; + let Ok(packet) = RebakeAvatarTexturesPacket::new_with_bytes_int32(data, &mut offset) else { + return; + }; + let texture_id = packet.texture_data.texture_id; + inner + .events + .rebake_avatar_requested + .emit_with(|| RebakeAvatarTexturesEventArgs { texture_id }); + spawn_appearance_request(inner, true, "appearance-rebake"); +} + +fn spawn_appearance_request(inner: &Arc, force_rebake: bool, name: &str) { + let manager = AppearanceManager::native_from_inner(Arc::clone(inner)); + let _ = std::thread::Builder::new() + .name(name.to_owned()) + .spawn(move || { + let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + else { + return; + }; + let _ = runtime.block_on(manager.native_request_set_appearance(force_rebake)); + }); +} + impl AppearanceManager { pub(crate) fn native_new(client: Option>) -> Result { let client = client.ok_or(Error::ArgumentNull)?; - Ok(Self::native_from_inner(Arc::new(AppearanceManagerInner { + let provider: Arc = Arc::new( + crate::GridClientBakingTextureProvider::native_new(client.as_ref().clone())?, + ); + let inner = Arc::new(AppearanceManagerInner { client: client.native_weak_handle(), wearables: RwLock::new(Vec::new()), attachments: RwLock::new(Vec::new()), last_cof_version: AtomicI32::new(-1), busy: AtomicBool::new(false), disposed: AtomicBool::new(false), + texture_provider: RwLock::new(provider), + texture_slots: RwLock::new( + (0..AvatarTextureIndex::NumberOfEntries as usize) + .map(|_| texture_data_defaults().expect("texture defaults")) + .collect(), + ), + wearable_assets: RwLock::new(HashMap::new()), + visual_parameters: RwLock::new(Vec::new()), + server_visual_parameters: AtomicBool::new(false), + cache_serial: AtomicI32::new(0), + cache_reply_generation: AtomicU64::new(0), + cache_notify: tokio::sync::Notify::new(), + wearables_reply_generation: AtomicU64::new(0), + wearables_notify: tokio::sync::Notify::new(), + appearance_serial: AtomicU64::new(0), + appearance_cancel: Mutex::new(None), + appearance_gate: Arc::new(tokio::sync::Semaphore::new(1)), events: AppearanceEvents::default(), - }))) + network_subscriptions: Mutex::new(Vec::new()), + }); + let weak = Arc::downgrade(&inner); + let subscription = client + .native_network()? + .subscribe_raw_packet(Arc::new(move |event| { + let Some(inner) = weak.upgrade() else { + return; + }; + handle_appearance_packet(&inner, event.packet_type, event.data); + })); + mutex(&inner.network_subscriptions).push(subscription); + Ok(Self::native_from_inner(inner)) } pub(crate) fn native_from_inner(inner: Arc) -> Self { + let my_visual_parameters = read(&inner.visual_parameters).clone(); Self { my_textures: PrimitiveTextureEntry { default_texture: None, face_textures: Vec::new(), }, - my_visual_parameters: Vec::new(), + my_visual_parameters, inner, } } @@ -230,11 +434,40 @@ impl AppearanceManager { self.inner.events.appearance_set.subscribe(handler) } + pub(crate) fn native_subscribe_cached_bakes_reply( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.cached_bakes_reply.subscribe(handler) + } + + pub(crate) fn native_subscribe_rebake_avatar_requested( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.rebake_avatar_requested.subscribe(handler) + } + + pub(crate) fn native_texture_provider(&self) -> Arc { + Arc::clone(&read(&self.inner.texture_provider)) + } + + pub(crate) fn native_set_texture_provider( + &self, + provider: Arc, + ) { + *write(&self.inner.texture_provider) = provider; + } + #[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible. pub(crate) fn native_dispose(&self) -> Result<(), Error> { self.inner.disposed.store(true, Ordering::Release); write(&self.inner.wearables).clear(); + self.inner + .server_visual_parameters + .store(false, Ordering::Release); write(&self.inner.attachments).clear(); + mutex(&self.inner.network_subscriptions).clear(); Ok(()) } @@ -264,11 +497,14 @@ impl AppearanceManager { apply_wearables(&mut next, items, replace)?; self.send_wearables(&next)?; *write(&self.inner.wearables) = next; + self.inner + .server_visual_parameters + .store(false, Ordering::Release); drop(busy); self.inner .events .appearance_set - .emit_with(|| AppearanceSetEventArgs); + .emit_with(|| AppearanceSetEventArgs { success: true }); self.inner .events .agent_wearables_reply @@ -283,11 +519,14 @@ impl AppearanceManager { next.retain(|wearable| !ids.contains(&wearable.item_id)); self.send_wearables(&next)?; *write(&self.inner.wearables) = next; + self.inner + .server_visual_parameters + .store(false, Ordering::Release); drop(busy); self.inner .events .appearance_set - .emit_with(|| AppearanceSetEventArgs); + .emit_with(|| AppearanceSetEventArgs { success: true }); self.inner .events .agent_wearables_reply @@ -314,11 +553,14 @@ impl AppearanceManager { apply_wearables(&mut next, items, false)?; self.send_wearables(&next)?; *write(&self.inner.wearables) = next; + self.inner + .server_visual_parameters + .store(false, Ordering::Release); drop(busy); self.inner .events .appearance_set - .emit_with(|| AppearanceSetEventArgs); + .emit_with(|| AppearanceSetEventArgs { success: true }); self.inner .events .agent_wearables_reply @@ -353,7 +595,13 @@ impl AppearanceManager { self.client()?; let values: Vec<_> = read(&self.inner.wearables) .iter() - .map(WearableRecord::public) + .map(|record| { + let mut value = record.public(); + value.asset = read(&self.inner.wearable_assets) + .get(&record.item_id) + .cloned(); + value + }) .collect(); Ok(Box::new(values.into_iter())) } @@ -377,7 +625,11 @@ impl AppearanceManager { self.client()?; let result = MultiValueDictionary::new_with_constructor()?; for wearable in read(&self.inner.wearables).iter() { - result.add(wearable.wearable_type, Some(wearable.public()))?; + let mut value = wearable.public(); + value.asset = read(&self.inner.wearable_assets) + .get(&wearable.item_id) + .cloned(); + result.add(wearable.wearable_type, Some(value))?; } Ok(result) } @@ -404,7 +656,7 @@ impl AppearanceManager { self.inner .events .appearance_set - .emit_with(|| AppearanceSetEventArgs); + .emit_with(|| AppearanceSetEventArgs { success: true }); Ok(()) } @@ -724,13 +976,735 @@ impl AppearanceManager { cancellation_token: Option, ) -> Result, Error> { let client = self.client()?; - let cof = CurrentOutfitFolder::new(Some(Arc::new(client)))?; - let links = cof - .get_current_outfit_links(cancellation_token.clone()) + let inventory = client.native_inventory()?; + let store = inventory.native_store().ok_or(Error::InvalidOperation)?; + let folder = self + .native_get_current_outfit_folder(cancellation_token.clone()) .await?; + let contents = if let Some(folder) = folder { + inventory + .native_folder_contents( + folder.base.uuid(), + folder.base.owner_id(), + true, + true, + InventorySortOrder::BY_DATE, + cancellation_token.clone(), + true, + ) + .await? + } else { + Vec::new() + }; + if contents.len() > MAX_COF_ENTRIES { + return Err(Error::InvalidOperation); + } check_cancelled(cancellation_token.as_ref())?; - Ok(links.into_iter().map(|item| item.base).collect()) + let mut worn = Vec::new(); + let mut wearables = Vec::new(); + let mut attachments = Vec::new(); + for value in contents { + check_cancelled(cancellation_token.as_ref())?; + let Some(link) = inventory_item(value.as_ref()) else { + continue; + }; + let target_id = if link.is_link()? { + link.asset_uuid() + } else { + link.base.uuid() + }; + let Some(item) = store.native_item_value(target_id) else { + continue; + }; + worn.push(item.base.clone()); + if let Some(record) = WearableRecord::from_item(&item) { + wearables.push(record); + } else if matches!(item.asset_type(), AssetType::Object | AssetType::Mesh) { + let point = if item.inventory_type() == InventoryType::ATTACHMENT { + InventoryAttachment { base: item.clone() }.attachment_point() + } else { + InventoryObject { base: item.clone() }.attach_point() + }; + attachments.push(AttachmentRecord { item, point }); + } + } + *write(&self.inner.wearables) = wearables; + self.inner + .server_visual_parameters + .store(false, Ordering::Release); + *write(&self.inner.attachments) = attachments; + self.inner + .events + .agent_wearables_reply + .emit_with(|| AgentWearablesReplyEventArgs); + Ok(worn) } + + #[allow(clippy::unnecessary_wraps)] // Public compatibility methods share the crate error model. + pub(crate) fn native_get_current_param_values(&self) -> Result, Error> { + Ok(crate::visual_catalog::decode_visual_params(&read( + &self.inner.visual_parameters, + ))) + } + + pub(crate) fn native_server_baking_region(&self) -> Result { + let protocols = self + .client()? + .native_network()? + .native_current_sim() + .map_or(0, |simulator| simulator.protocols.0); + Ok(protocols & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0) + } + + fn native_server_baking_available(&self) -> Result { + if !self.native_server_baking_region()? { + return Ok(false); + } + let simulator = self + .client()? + .native_network()? + .native_current_sim() + .ok_or(Error::InvalidOperation)?; + let Some(caps) = simulator.caps.as_ref() else { + return Ok(false); + }; + Ok(caps + .capability_uri("UpdateAvatarAppearance".to_owned())? + .is_some()) + } + + fn native_request_cached_bakes_sent(&self) -> Result { + let client = self.client()?; + let network = client.native_network()?; + let mut packet = AgentCachedTexturePacket::new_with_constructor()?; + packet.agent_data.agent_id = network.native_agent_id(); + packet.agent_data.session_id = network.native_session_id(); + packet.agent_data.serial_num = self.inner.cache_serial.fetch_add(1, Ordering::AcqRel) + 1; + let wearables = read(&self.inner.wearables); + let map = wearable_bake_map(); + let salts = baked_texture_hash(); + for baked_index in 0..11_usize { + if baked_index == BakeType::Skirt.0 as usize + && !wearables + .iter() + .any(|item| item.wearable_type == WearableType::Skirt) + { + continue; + } + let mut hash = UUID::zero(); + for wearable_type in &map[baked_index] { + for item in wearables + .iter() + .filter(|item| item.wearable_type == *wearable_type) + { + hash = UUID::bitxor(hash, item.asset_id); + } + } + if hash != UUID::zero() { + let mut block = AgentCachedTexturePacketWearableDataBlock::new_with_constructor()?; + block.id = UUID::bitxor(hash, salts[baked_index]); + block.texture_index = u8::try_from(baked_index).map_err(|_| Error::Argument)?; + packet.wearable_data.push(block); + } + } + drop(wearables); + if packet.wearable_data.is_empty() { + return Ok(false); + } + send_generated( + &network, + PacketType::AgentCachedTexture, + packet.to_bytes_with_method()?, + )?; + Ok(true) + } + + pub(crate) fn native_request_cached_bakes(&self) -> Result<(), Error> { + self.native_request_cached_bakes_sent().map(|_| ()) + } + + async fn native_gather_legacy_wearables(&self, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested()?; + let network = self.client()?.native_network()?; + let generation = self + .inner + .wearables_reply_generation + .load(Ordering::Acquire); + let notified = self.inner.wearables_notify.notified(); + let mut packet = AgentWearablesRequestPacket::new_with_constructor()?; + packet.agent_data.agent_id = network.native_agent_id(); + packet.agent_data.session_id = network.native_session_id(); + send_generated( + &network, + PacketType::AgentWearablesRequest, + packet.to_bytes_with_method()?, + )?; + if self + .inner + .wearables_reply_generation + .load(Ordering::Acquire) + == generation + { + tokio::select! { + () = notified => {}, + () = token.cancelled() => return Err(Error::Cancelled), + () = tokio::time::sleep(std::time::Duration::from_secs(30)) => {}, + } + } + token.throw_if_cancellation_requested()?; + if read(&self.inner.wearables).is_empty() { + Err(Error::InvalidOperation) + } else { + Ok(()) + } + } + + async fn native_refresh_worn_for_bake( + &self, + use_server_baking: bool, + token: CancellationToken, + ) -> Result<(), Error> { + match self.native_request_agent_worn(Some(token.clone())).await { + Ok(_) => {} + Err(Error::Cancelled) => return Err(Error::Cancelled), + Err(error) if use_server_baking => return Err(error), + Err(_) => { + write(&self.inner.wearables).clear(); + write(&self.inner.attachments).clear(); + } + } + if !use_server_baking && read(&self.inner.wearables).is_empty() { + self.native_gather_legacy_wearables(token).await?; + } + Ok(()) + } + + pub(crate) fn native_make_appearance_packet(&self) -> Result { + let client = self.client()?; + let network = client.native_network()?; + let mut packet = AgentSetAppearancePacket::new_with_constructor()?; + packet.agent_data.agent_id = network.native_agent_id(); + packet.agent_data.session_id = network.native_session_id(); + packet.agent_data.serial_num = + u32::try_from(self.inner.appearance_serial.fetch_add(1, Ordering::AcqRel) + 1) + .unwrap_or(u32::MAX); + + let assets = read(&self.inner.wearable_assets); + let server_visual = read(&self.inner.visual_parameters).clone(); + let (bytes, param_values) = if self.inner.server_visual_parameters.load(Ordering::Acquire) + && !server_visual.is_empty() + { + let values = crate::visual_catalog::decode_visual_params(&server_visual); + (server_visual, values) + } else { + let params = VisualParams::params(); + let wearing_physics = read(&self.inner.wearables) + .iter() + .any(|wearable| wearable.wearable_type == WearableType::Physics); + let parameter_count = if wearing_physics { 251 } else { 218 }; + let mut values = HashMap::new(); + let bytes = VisualParams::group0_param_ids() + .into_iter() + .take(parameter_count) + .filter_map(|param_id| { + let param = params.0.get(¶m_id)?; + let param_value = assets + .values() + .find_map(|asset| asset.params.get(¶m_id).copied()) + .unwrap_or(param.default_value); + values.insert(param_id, param_value); + let range = param.max_value - param.min_value; + Some(if range.abs() <= f32::EPSILON { + 0 + } else { + quantize_visual_param((param_value - param.min_value) / range) + }) + }) + .collect(); + (bytes, values) + }; + drop(assets); + for value in bytes.iter().copied() { + let mut block = AgentSetAppearancePacketVisualParamBlock::new_with_constructor()?; + block.param_value = value; + packet.visual_param.push(block); + } + *write(&self.inner.visual_parameters) = bytes; + + let texture_ids: Vec<_> = read(&self.inner.texture_slots) + .iter() + .map(|slot| slot.texture_id) + .collect(); + packet.object_data.texture_entry = encode_texture_entry(&texture_ids)?; + let map = wearable_bake_map(); + let salts = baked_texture_hash(); + let wearables = read(&self.inner.wearables); + for baked_index in 0..11_usize { + let mut hash = UUID::zero(); + for wearable_type in &map[baked_index] { + for item in wearables + .iter() + .filter(|item| item.wearable_type == *wearable_type) + { + hash = UUID::bitxor(hash, item.asset_id); + } + } + if hash != UUID::zero() { + hash = UUID::bitxor(hash, salts[baked_index]); + } + let mut block = AgentSetAppearancePacketWearableDataBlock::new_with_constructor()?; + block.cache_id = hash; + block.texture_index = bake_index_to_texture_index()[baked_index]; + packet.wearable_data.push(block); + } + let value = |id, default| param_values.get(&id).copied().unwrap_or(default); + let height = 1.706 + + value(692, 0.0) * 0.1918 + + value(842, 0.0) * 0.0375 + + value(33, 0.0) * 0.12022 + + value(682, 0.5) * 0.01117 + + value(756, 0.0) * 0.038 + + value(198, 0.0) * 0.08 + + value(503, 0.0) * 0.07; + packet.agent_data.size = Vector3::new_with_single_single_single(0.45, 0.6, height)?; + Ok(packet) + } + + pub(crate) async fn native_request_set_appearance( + &self, + force_rebake: bool, + ) -> Result<(), Error> { + let source = CancellationTokenSource::new(); + if let Some(previous) = mutex(&self.inner.appearance_cancel).replace(source.clone()) { + previous.cancel(); + } + let token = source.token(); + let _permit = self + .inner + .appearance_gate + .acquire() + .await + .map_err(|_| Error::InvalidOperation)?; + token.throw_if_cancellation_requested()?; + self.inner.busy.store(true, Ordering::Release); + let _busy = BusyGuard(&self.inner.busy); + let old_slots = clone_texture_slots(&read(&self.inner.texture_slots))?; + let old_assets = read(&self.inner.wearable_assets).clone(); + let old_visual_parameters = read(&self.inner.visual_parameters).clone(); + let old_server_visual = self.inner.server_visual_parameters.load(Ordering::Acquire); + let old_cof_version = self.inner.last_cof_version.load(Ordering::Acquire); + let result = async { + let start_simulator = self + .client()? + .native_network()? + .native_current_sim() + .ok_or(Error::InvalidOperation)?; + let use_server_baking = self.native_server_baking_available()?; + self.native_refresh_worn_for_bake(use_server_baking, token.clone()) + .await?; + if force_rebake { + let mut slots = write(&self.inner.texture_slots); + for target in bake_index_to_texture_index() { + if let Some(slot) = slots.get_mut(usize::from(target)) { + slot.texture_id = UUID::zero(); + slot.texture = None; + } + } + } + if use_server_baking { + self.native_server_bake(token.clone()).await?; + } else { + if !force_rebake && !read(&self.inner.wearables).is_empty() { + let generation = self.inner.cache_reply_generation.load(Ordering::Acquire); + let notified = self.inner.cache_notify.notified(); + let request_sent = self.native_request_cached_bakes_sent()?; + if request_sent + && self.inner.cache_reply_generation.load(Ordering::Acquire) == generation + { + tokio::select! { + () = notified => {}, + () = token.cancelled() => return Err(Error::Cancelled), + () = tokio::time::sleep(std::time::Duration::from_secs(30)) => {}, + } + } + let _cache_reply_received = + self.inner.cache_reply_generation.load(Ordering::Acquire) != generation; + } + self.native_gather_baking_inputs(token.clone()).await?; + self.native_local_bake(Some(token.clone()), force_rebake) + .await?; + } + token.throw_if_cancellation_requested()?; + let packet = self.native_make_appearance_packet()?; + let network = self.client()?.native_network()?; + if network.native_current_sim().as_ref() != Some(&start_simulator) { + return Err(Error::InvalidOperation); + } + send_generated( + &network, + PacketType::AgentSetAppearance, + packet.to_bytes_with_method()?, + ) + } + .await; + if result.is_err() { + *write(&self.inner.texture_slots) = old_slots; + *write(&self.inner.wearable_assets) = old_assets; + *write(&self.inner.visual_parameters) = old_visual_parameters; + self.inner + .server_visual_parameters + .store(old_server_visual, Ordering::Release); + self.inner + .last_cof_version + .store(old_cof_version, Ordering::Release); + } + // Never invoke callbacks while the manager's state locks are held. + let success = result.is_ok(); + self.inner + .events + .appearance_set + .emit_with(|| AppearanceSetEventArgs { success }); + result + } + + async fn native_server_bake(&self, token: CancellationToken) -> Result<(), Error> { + let cof = self + .native_get_current_outfit_folder(Some(token.clone())) + .await? + .ok_or(Error::InvalidOperation)?; + if cof.version() < 0 { + return Err(Error::InvalidOperation); + } + let simulator = self + .client()? + .native_network()? + .native_current_sim() + .ok_or(Error::InvalidOperation)?; + let capability = simulator + .caps + .as_ref() + .ok_or(Error::InvalidOperation)? + .capability_uri("UpdateAvatarAppearance".to_owned())? + .ok_or(Error::InvalidOperation)?; + for retry in 0..5_u32 { + token.throw_if_cancellation_requested()?; + if retry > 0 { + let delay = (1_u64 << retry).saturating_sub(1); + tokio::select! { + () = tokio::time::sleep(std::time::Duration::from_secs(delay)) => {}, + () = token.cancelled() => return Err(Error::Cancelled), + } + } + let payload = OSD::Map(HashMap::from([( + "cof_version".to_owned(), + OSD::Integer(cof.version()), + )])); + let (_, bytes) = self + .client()? + .native_http_caps_client() + .post_with_uri_osd_format_osd_cancellation_token_i_progress( + capability.clone(), + OSDFormat::Xml, + payload, + token.clone(), + None, + ) + .await?; + let OSD::Map(result) = OSDParser::deserialize_with_bytes(bytes)? else { + continue; + }; + if !result + .get("success") + .is_some_and(|value| value.as_boolean().unwrap_or(false)) + { + continue; + } + let visual = result + .get("visual_params") + .map(OSD::as_binary) + .transpose()? + .unwrap_or_default(); + let textures = match result.get("textures") { + Some(OSD::Array(values)) => values + .iter() + .map(OSD::as_uuid) + .collect::, _>>()?, + _ => Vec::new(), + }; + let required = [8_usize, 9, 10, 11]; + if required.iter().any(|&index| { + textures + .get(index) + .is_none_or(|id| *id == UUID::zero() || *id == default_avatar_texture()) + }) { + continue; + } + let mut slots: Vec<_> = (0..AvatarTextureIndex::NumberOfEntries as usize) + .map(|_| texture_data_defaults().expect("texture defaults")) + .collect(); + for (index, id) in textures.into_iter().take(slots.len()).enumerate() { + slots[index].texture_id = id; + slots[index].texture_index = i32::try_from(index) + .ok() + .and_then(avatar_texture_index_from_i32) + .unwrap_or(AvatarTextureIndex::Unknown); + } + *write(&self.inner.texture_slots) = slots; + *write(&self.inner.visual_parameters) = visual; + self.inner + .server_visual_parameters + .store(true, Ordering::Release); + if let Some(version) = result + .get("cof_version") + .and_then(|value| value.as_integer().ok()) + { + self.inner + .last_cof_version + .fetch_max(version, Ordering::AcqRel); + } + return Ok(()); + } + Err(Error::InvalidOperation) + } + + async fn native_gather_baking_inputs(&self, token: CancellationToken) -> Result<(), Error> { + let records = read(&self.inner.wearables).clone(); + let assets = self.client()?.native_assets()?; + let mut decoded = HashMap::new(); + let mut slots: Vec<_> = (0..AvatarTextureIndex::NumberOfEntries as usize) + .map(|_| texture_data_defaults().expect("texture defaults")) + .collect(); + { + let current = read(&self.inner.texture_slots); + for target in bake_index_to_texture_index() { + if let (Some(source), Some(destination)) = ( + current.get(usize::from(target)), + slots.get_mut(usize::from(target)), + ) { + destination.texture_id = source.texture_id; + destination.texture_index = source.texture_index; + } + } + } + for record in records { + token.throw_if_cancellation_requested()?; + let Some(asset) = assets + .request_asset_with_uuid_asset_type_boolean_cancellation_token( + record.asset_id, + record.asset_type, + true, + Some(token.clone()), + ) + .await? + else { + return Err(Error::InvalidOperation); + }; + let wearable_asset = + crate::assets::AssetWearable::native_from_asset(&asset, record.wearable_type)?; + let public = AppearanceManagerWearableData { + asset: Some(wearable_asset.clone()), + asset_id: record.asset_id, + asset_type: record.asset_type, + item_id: record.item_id, + wearable_type: record.wearable_type, + }; + decode_wearable_params(public, &mut slots)?; + decoded.insert(record.item_id, wearable_asset); + } + + let provider = self.native_texture_provider(); + let semaphore = Arc::new(tokio::sync::Semaphore::new(5)); + let mut requests = FuturesUnordered::new(); + for (index, slot) in slots.iter().enumerate() { + if slot.texture_id == UUID::zero() { + continue; + } + let provider = Arc::clone(&provider); + let semaphore = Arc::clone(&semaphore); + let request_token = token.clone(); + let texture_id = slot.texture_id; + requests.push(async move { + let _permit = semaphore + .acquire_owned() + .await + .map_err(|_| Error::InvalidOperation)?; + request_token.throw_if_cancellation_requested()?; + let texture = provider + .request_texture(texture_id, Some(request_token)) + .await? + .ok_or(Error::InvalidOperation)?; + Ok::<_, Error>((index, texture)) + }); + } + while let Some(result) = requests.next().await { + let (index, texture) = result?; + slots[index].texture = Some(texture); + } + token.throw_if_cancellation_requested()?; + *write(&self.inner.texture_slots) = slots; + *write(&self.inner.wearable_assets) = decoded; + self.inner + .server_visual_parameters + .store(false, Ordering::Release); + Ok(()) + } + + async fn native_local_bake( + &self, + cancellation_token: Option, + force_rebake: bool, + ) -> Result<(), Error> { + check_cancelled(cancellation_token.as_ref())?; + // Texture decoding populates these slots. Baking an empty outfit is a + // valid recovery path and simply retains the previous uploaded bakes. + let indices: Vec<_> = read(&self.inner.texture_slots) + .iter() + .enumerate() + .filter_map(|(index, slot)| slot.texture.as_ref().map(|_| index)) + .collect(); + if indices.is_empty() { + return Ok(()); + } + let mut pending_uploads = Vec::new(); + for bake_index in 0..11_i32 { + check_cancelled(cancellation_token.as_ref())?; + let bake_type = BakeType(bake_index); + let target_index = bake_type_to_agent_texture_index(bake_type); + if !force_rebake + && read(&self.inner.texture_slots) + .get(target_index as usize) + .is_some_and(|slot| slot.texture_id != UUID::zero()) + { + continue; + } + let required = bake_type_to_textures(bake_type); + let mut compositor = crate::appearance_baker::Baker::native_new(bake_type)?; + let mut layer_count = 0; + for index in required { + if let Some(slot) = read(&self.inner.texture_slots).get(index as usize) + && slot.texture.is_some() + && let Some(copy) = clone_texture_slot(slot)? + { + compositor.native_add_texture(copy)?; + layer_count += 1; + } + } + if layer_count == 0 { + continue; + } + compositor.native_bake()?; + let Some(baked_texture) = compositor.native_baked_texture() else { + continue; + }; + pending_uploads.push((bake_type_to_agent_texture_index(bake_type), baked_texture)); + } + let assets = self.client()?.native_assets()?; + let upload_gate = Arc::new(tokio::sync::Semaphore::new(6)); + let mut uploads = FuturesUnordered::new(); + for (target, baked_texture) in pending_uploads { + let assets = assets.clone(); + let gate = Arc::clone(&upload_gate); + let token = cancellation_token.clone(); + uploads.push(async move { + let _permit = gate + .acquire_owned() + .await + .map_err(|_| Error::InvalidOperation)?; + check_cancelled(token.as_ref())?; + let uploaded = assets + .request_upload_baked_texture(baked_texture.native_bytes(), token) + .await?; + Ok::<_, Error>((target, uploaded, baked_texture)) + }); + } + while let Some(result) = uploads.next().await { + let (target, uploaded, baked_texture) = result?; + if uploaded == UUID::zero() { + return Err(Error::InvalidOperation); + } + if let Some(slot) = write(&self.inner.texture_slots).get_mut(target as usize) { + slot.texture_id = uploaded; + slot.texture = Some(baked_texture); + slot.texture_index = target; + } + } + Ok(()) + } +} + +fn clone_texture_slot( + slot: &AppearanceManagerTextureData, +) -> Result, Error> { + Ok(Some(AppearanceManagerTextureData { + alpha_masks: slot.alpha_masks.clone(), + color: slot.color, + texture: slot + .texture + .as_ref() + .map(crate::assets::AssetTexture::native_deep_clone) + .transpose()?, + texture_id: slot.texture_id, + texture_index: slot.texture_index, + })) +} + +fn clone_texture_slots( + slots: &[AppearanceManagerTextureData], +) -> Result, Error> { + slots + .iter() + .map(|slot| clone_texture_slot(slot)?.ok_or(Error::InvalidOperation)) + .collect() +} + +fn encode_texture_entry(texture_ids: &[UUID]) -> Result, Error> { + let default = default_avatar_texture(); + let mut output = default.get_bytes()?; + for (face, id) in texture_ids.iter().copied().enumerate() { + if id == UUID::zero() || id == default { + continue; + } + let mask = 1_u64 + .checked_shl(u32::try_from(face).map_err(|_| Error::Argument)?) + .ok_or(Error::Argument)?; + output.extend_from_slice(&texture_entry_face_bitfield(mask)); + output.extend_from_slice(&id.get_bytes()?); + } + output.push(0); // Texture exception terminator. + + // Remaining default face attributes and their zero exception terminators. + // BinaryWriter in the reference uses little-endian numeric values. + output.extend_from_slice(&[0, 0, 0, 0, 0]); // inverted white RGBA + terminator + output.extend_from_slice(&1.0_f32.to_le_bytes()); + output.push(0); // RepeatU + output.extend_from_slice(&1.0_f32.to_le_bytes()); + output.push(0); // RepeatV + for _ in 0..3 { + output.extend_from_slice(&0_i16.to_le_bytes()); + output.push(0); // OffsetU, OffsetV, Rotation + } + output.extend_from_slice(&[0, 0]); // material + terminator + output.extend_from_slice(&[0, 0]); // media + terminator + output.extend_from_slice(&[0, 0]); // glow + terminator + output.extend_from_slice(&UUID::zero().get_bytes()?); + output.push(0); // material ID terminator + output.extend_from_slice(&UUID::zero().get_bytes()?); // render material ID + Ok(output) +} + +fn texture_entry_face_bitfield(mask: u64) -> Vec { + let byte_count = usize::try_from((u64::BITS - mask.leading_zeros()).div_ceil(7)).unwrap_or(1); + (0..byte_count) + .map(|index| { + let shift = 7 * (byte_count - index - 1); + let byte = u8::try_from((mask >> shift) & 0x7f).unwrap_or(0); + if index + 1 < byte_count { + byte | 0x80 + } else { + byte + } + }) + .collect() } impl PartialEq for AppearanceManagerWearableData { @@ -744,6 +1718,440 @@ impl PartialEq for AppearanceManagerWearableData { impl Eq for AppearanceManagerWearableData {} +#[allow(clippy::unnecessary_wraps)] // The generated constructor is fallible across the API. +pub(crate) fn texture_data_defaults() -> Result { + Ok(AppearanceManagerTextureData { + alpha_masks: HashMap::new(), + color: Color4::white(), + texture: None, + texture_id: UUID::zero(), + texture_index: AvatarTextureIndex::Unknown, + }) +} + +pub(crate) fn texture_data_to_string(value: &AppearanceManagerTextureData) -> String { + format!("{:?}: {}", value.texture_index, value.texture_id) +} + +#[allow(clippy::unnecessary_wraps)] // The generated constructor is fallible across the API. +pub(crate) fn wearable_data_defaults() -> Result { + Ok(AppearanceManagerWearableData { + asset: None, + asset_id: UUID::zero(), + asset_type: AssetType::Unknown, + item_id: UUID::zero(), + wearable_type: WearableType::Invalid, + }) +} + +pub(crate) fn wearable_data_to_string(value: &AppearanceManagerWearableData) -> String { + format!( + "{:?}: item {}, asset {}", + value.wearable_type, value.item_id, value.asset_id + ) +} + +impl Eq for crate::VisualAlphaParam {} +impl Hash for crate::VisualAlphaParam { + fn hash(&self, state: &mut H) { + self.domain.to_bits().hash(state); + self.multiply_blend.hash(state); + self.skip_if_zero.hash(state); + self.tga_file.hash(state); + } +} + +#[allow(clippy::needless_pass_by_value)] // Mirrors the by-value compatibility signature. +pub(crate) fn decode_wearable_params( + wearable: AppearanceManagerWearableData, + textures: &mut Vec, +) -> Result<(), Error> { + let Some(asset) = wearable.asset.as_ref() else { + return Ok(()); + }; + if textures.len() < AvatarTextureIndex::NumberOfEntries as usize { + textures.resize_with(AvatarTextureIndex::NumberOfEntries as usize, || { + texture_data_defaults().expect("texture defaults") + }); + } + let params = VisualParams::params(); + let mut masks = HashMap::new(); + let mut colors = Vec::new(); + for (¶m_id, &value) in &asset.params { + let Some(param) = params.0.get(¶m_id) else { + continue; + }; + if let Some(Some(color)) = ¶m.color_params { + let accepted = match wearable.wearable_type { + WearableType::Tattoo => matches!(param_id, 1062..=1064), + WearableType::Jacket => matches!(param_id, 809..=811), + WearableType::Hair => matches!(param_id, 112..=115), + WearableType::Skin => matches!(param_id, 108 | 110 | 111), + _ => true, + }; + if accepted { + colors.push(crate::AppearanceManagerColorParamInfo { + value, + visual_color_param: color.clone(), + visual_param: param.clone(), + wearable_type: wearable.wearable_type, + }); + } + } + if let Some(Some(alpha)) = ¶m.alpha_params + && !alpha.tga_file.is_empty() + && !param.is_bump_attribute + { + masks + .entry(alpha.clone()) + .or_insert(if value.abs() < f32::EPSILON { + 0.01 + } else { + value + }); + } + if let Some(drivers) = ¶m.drivers { + for driver_id in drivers { + if let Some(driver) = params.0.get(driver_id) + && let Some(Some(alpha)) = &driver.alpha_params + && !alpha.tga_file.is_empty() + && !driver.is_bump_attribute + { + masks + .entry(alpha.clone()) + .or_insert(if value.abs() < f32::EPSILON { + 0.01 + } else { + value + }); + } + } + } + } + let color = if colors.is_empty() { + Color4::white() + } else { + color_from_params(colors)? + }; + for (&index, &texture_id) in &asset.textures { + let slot_index = index as usize; + let Some(slot) = textures.get_mut(slot_index) else { + continue; + }; + slot.texture_index = index; + slot.alpha_masks.clone_from(&masks); + slot.color = color; + let id = if texture_id == default_avatar_texture() { + UUID::zero() + } else { + texture_id + }; + if slot.texture_id != id { + slot.texture_id = id; + slot.texture = None; + } + } + Ok(()) +} + +#[allow( + clippy::cast_possible_truncation, + clippy::cast_precision_loss, + clippy::cast_sign_loss +)] // Reference color ramps select adjacent entries using normalized f32 positions. +pub(crate) fn color_from_params( + params: Vec, +) -> Result { + let mut result = Color4 { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + for param in params { + let colors = ¶m.visual_color_param.colors; + if colors.is_empty() { + continue; + } + let selected = if colors.len() == 1 { + colors[0] + } else { + let range = param.visual_param.max_value - param.visual_param.min_value; + let normalized = if range.abs() <= f32::EPSILON { + 0.0 + } else { + ((param.value - param.visual_param.min_value) / range).clamp(0.0, 1.0) + }; + let position = normalized * (colors.len() - 1) as f32; + let lower = position.floor() as usize; + let upper = (lower + 1).min(colors.len() - 1); + Color4::lerp(colors[lower], colors[upper], position - lower as f32)? + }; + result = match param.visual_color_param.operation { + crate::VisualColorOperation::Add => Color4::add(result, selected), + crate::VisualColorOperation::Multiply => Color4::mul(result, selected), + crate::VisualColorOperation::Blend => Color4::lerp(result, selected, param.value)?, + }; + } + Ok(result) +} + +pub(crate) fn bake_index_to_texture_index() -> Vec { + vec![8, 9, 10, 11, 19, 20, 40, 41, 42, 43, 44] +} + +pub(crate) fn avatar_texture_index_from_i32(value: i32) -> Option { + use AvatarTextureIndex as T; + let values = [ + T::HeadBodypaint, + T::UpperShirt, + T::LowerPants, + T::EyesIris, + T::Hair, + T::UpperBodypaint, + T::LowerBodypaint, + T::LowerShoes, + T::HeadBaked, + T::UpperBaked, + T::LowerBaked, + T::EyesBaked, + T::LowerSocks, + T::UpperJacket, + T::LowerJacket, + T::UpperGloves, + T::UpperUndershirt, + T::LowerUnderpants, + T::Skirt, + T::SkirtBaked, + T::HairBaked, + T::LowerAlpha, + T::UpperAlpha, + T::HeadAlpha, + T::EyesAlpha, + T::HairAlpha, + T::HeadTattoo, + T::UpperTattoo, + T::LowerTattoo, + T::HeadUniversalTattoo, + T::UpperUniversalTattoo, + T::LowerUniversalTattoo, + T::SkirtTattoo, + T::HairTattoo, + T::EyesTattoo, + T::LeftArmTattoo, + T::LeftLegTattoo, + T::Aux1Tattoo, + T::Aux2Tattoo, + T::Aux3Tattoo, + T::LeftArmBaked, + T::LegLegBaked, + T::Aux1Baked, + T::Aux2Baked, + T::Aux3Baked, + ]; + usize::try_from(value) + .ok() + .and_then(|index| values.get(index).copied()) +} + +fn uuid(value: &str) -> UUID { + UUID::new_with_string(value.to_owned()).expect("fixed appearance UUID") +} + +#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +fn quantize_visual_param(value: f32) -> u8 { + (value.clamp(0.0, 1.0) * 255.0).round() as u8 +} + +pub(crate) fn baked_texture_hash() -> Vec { + [ + "18ded8d6-bcfc-e415-8539-944c0f5ea7a6", + "338c29e3-3024-4dbb-998d-7c04cf4fa88f", + "91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f", + "b2cf28af-b840-1071-3c6a-78085d8128b5", + "ea800387-ea1a-14e0-56cb-24f2022f969a", + "0af1ef7c-ad24-11dd-8790-001f5bf833e8", + "9d762b57-ffe3-2e34-d897-0c44c8e07c72", + "e12f6f01-8b0e-e00a-03c7-bc7e56a6cbdc", + "3e2984a2-f03c-71d5-3e97-75fb8c7e1e2f", + "29bbb16c-4b0c-4809-8de5-4b4df7cca8ef", + "e0f8b768-e68d-a0cc-d1d8-c2e7f3b51b6e", + ] + .into_iter() + .map(uuid) + .collect() +} + +pub(crate) fn default_avatar_texture() -> UUID { + uuid("c228d1cf-4b5d-4ba8-84f4-899a0796aa97") +} + +pub(crate) fn img_use_baked(bake: BakeType) -> UUID { + let value = match bake { + BakeType::Head => "5a9f4a74-30f2-821c-b88d-70499d3e7183", + BakeType::UpperBody => "ae2de45c-d252-50b8-5c6e-19f39ce79317", + BakeType::LowerBody => "24daea5f-0539-cfcf-047f-fbc40b2786ba", + BakeType::Eyes => "52cc6bb6-2ee5-e632-d3ad-50197b1dcb8a", + BakeType::Skirt => "43529ce8-7faa-ad92-165a-bc4078371687", + BakeType::Hair => "09aac1fb-6bce-0bee-7d44-caac6dbb6c63", + BakeType::BakedLeftArm => "ff62763f-d60a-9855-890b-0c96f8f8cd98", + BakeType::BakedLeftLeg => "8e915e25-31d1-cc95-ae08-d58a47488251", + BakeType::BakedAux1 => "9742065b-19b5-297c-858a-29711d539043", + BakeType::BakedAux2 => "03642e83-2bd1-4eb9-34b4-4c47ed586d2d", + BakeType::BakedAux3 => "edd51b77-fc10-ce7a-4b3d-011dfc349e4f", + _ => return UUID::zero(), + }; + uuid(value) +} + +pub(crate) fn img_use_baked_indices() +-> libremetaverse_types::compat::FrozenDictionary { + libremetaverse_types::compat::FrozenDictionary(HashMap::from([ + (img_use_baked(BakeType::Head), AvatarTextureIndex::HeadBaked), + ( + img_use_baked(BakeType::UpperBody), + AvatarTextureIndex::UpperBaked, + ), + ( + img_use_baked(BakeType::LowerBody), + AvatarTextureIndex::LowerBaked, + ), + (img_use_baked(BakeType::Eyes), AvatarTextureIndex::EyesBaked), + ( + img_use_baked(BakeType::Skirt), + AvatarTextureIndex::SkirtBaked, + ), + (img_use_baked(BakeType::Hair), AvatarTextureIndex::HairBaked), + ( + img_use_baked(BakeType::BakedLeftArm), + AvatarTextureIndex::LeftArmBaked, + ), + ( + img_use_baked(BakeType::BakedLeftLeg), + AvatarTextureIndex::LegLegBaked, + ), + ( + img_use_baked(BakeType::BakedAux1), + AvatarTextureIndex::Aux1Baked, + ), + ( + img_use_baked(BakeType::BakedAux2), + AvatarTextureIndex::Aux2Baked, + ), + ( + img_use_baked(BakeType::BakedAux3), + AvatarTextureIndex::Aux3Baked, + ), + ])) +} + +pub(crate) fn wearable_bake_map() -> Vec> { + use WearableType::{ + Alpha, Eyes, Gloves, Hair, Invalid, Jacket, Pants, Shape, Shirt, Shoes, Skin, Skirt, Socks, + Tattoo, Underpants, Undershirt, + }; + vec![ + vec![ + Shape, Skin, Tattoo, Hair, Alpha, Invalid, Invalid, Invalid, Invalid, + ], + vec![ + Shape, Skin, Tattoo, Shirt, Jacket, Gloves, Undershirt, Alpha, Invalid, + ], + vec![ + Shape, Skin, Tattoo, Pants, Shoes, Socks, Jacket, Underpants, Alpha, + ], + vec![ + Eyes, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, + ], + vec![ + Skirt, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, + ], + vec![ + Hair, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, + ], + vec![Invalid; 9], + vec![Invalid; 9], + vec![Invalid; 9], + vec![Invalid; 9], + vec![Invalid; 9], + ] +} + +pub(crate) const fn bake_type_to_agent_texture_index(bake: BakeType) -> AvatarTextureIndex { + match bake { + BakeType::Head => AvatarTextureIndex::HeadBaked, + BakeType::UpperBody => AvatarTextureIndex::UpperBaked, + BakeType::LowerBody => AvatarTextureIndex::LowerBaked, + BakeType::Eyes => AvatarTextureIndex::EyesBaked, + BakeType::Skirt => AvatarTextureIndex::SkirtBaked, + BakeType::Hair => AvatarTextureIndex::HairBaked, + BakeType::BakedLeftArm => AvatarTextureIndex::LeftArmBaked, + BakeType::BakedLeftLeg => AvatarTextureIndex::LegLegBaked, + BakeType::BakedAux1 => AvatarTextureIndex::Aux1Baked, + BakeType::BakedAux2 => AvatarTextureIndex::Aux2Baked, + BakeType::BakedAux3 => AvatarTextureIndex::Aux3Baked, + _ => AvatarTextureIndex::Unknown, + } +} + +pub(crate) fn bake_type_to_textures(bake: BakeType) -> Vec { + use AvatarTextureIndex as T; + match bake { + BakeType::Head => vec![ + T::HeadBodypaint, + T::HeadTattoo, + T::HeadUniversalTattoo, + T::Hair, + T::HeadAlpha, + ], + BakeType::UpperBody => vec![ + T::UpperBodypaint, + T::UpperTattoo, + T::UpperUniversalTattoo, + T::UpperGloves, + T::UpperUndershirt, + T::UpperShirt, + T::UpperJacket, + T::UpperAlpha, + ], + BakeType::LowerBody => vec![ + T::LowerBodypaint, + T::LowerTattoo, + T::LowerUniversalTattoo, + T::LowerUnderpants, + T::LowerSocks, + T::LowerShoes, + T::LowerPants, + T::LowerJacket, + T::LowerAlpha, + ], + BakeType::Eyes => vec![T::EyesIris, T::EyesTattoo, T::EyesAlpha], + BakeType::Skirt => vec![T::Skirt, T::SkirtTattoo], + BakeType::Hair => vec![T::Hair, T::HairTattoo, T::HairAlpha], + BakeType::BakedLeftArm => vec![T::LeftArmTattoo], + BakeType::BakedLeftLeg => vec![T::LeftLegTattoo], + BakeType::BakedAux1 => vec![T::Aux1Tattoo], + BakeType::BakedAux2 => vec![T::Aux2Tattoo], + BakeType::BakedAux3 => vec![T::Aux3Tattoo], + _ => Vec::new(), + } +} + +pub(crate) const fn morph_layer_for_bake_type(bake: BakeType) -> AvatarTextureIndex { + match bake { + BakeType::Head | BakeType::Hair => AvatarTextureIndex::Hair, + BakeType::UpperBody => AvatarTextureIndex::UpperShirt, + BakeType::LowerBody => AvatarTextureIndex::LowerPants, + BakeType::Skirt => AvatarTextureIndex::Skirt, + BakeType::BakedLeftArm => AvatarTextureIndex::LeftArmTattoo, + BakeType::BakedLeftLeg => AvatarTextureIndex::LeftLegTattoo, + BakeType::BakedAux1 => AvatarTextureIndex::Aux1Tattoo, + BakeType::BakedAux2 => AvatarTextureIndex::Aux2Tattoo, + BakeType::BakedAux3 => AvatarTextureIndex::Aux3Tattoo, + _ => AvatarTextureIndex::Unknown, + } +} + pub(crate) const fn wearable_type_to_asset_type(wearable_type: WearableType) -> AssetType { match wearable_type { WearableType::Shape | WearableType::Skin | WearableType::Hair | WearableType::Eyes => { @@ -754,6 +2162,32 @@ pub(crate) const fn wearable_type_to_asset_type(wearable_type: WearableType) -> } } +fn wearable_type_from_u8(value: u8) -> Option { + use WearableType as W; + [ + W::Shape, + W::Skin, + W::Hair, + W::Eyes, + W::Shirt, + W::Pants, + W::Shoes, + W::Socks, + W::Jacket, + W::Gloves, + W::Undershirt, + W::Underpants, + W::Skirt, + W::Alpha, + W::Tattoo, + W::Physics, + W::Universal, + ] + .get(usize::from(value)) + .copied() + .or((value == u8::MAX).then_some(W::Invalid)) +} + fn apply_wearables( current: &mut Vec, items: Vec, @@ -961,4 +2395,155 @@ mod tests { assert_eq!(state.len(), 1); assert_eq!(state.get(&replacement_id), Some(&AttachmentPoint::Skull)); } + + #[test] + fn appearance_protocol_constants_and_event_payloads_preserve_reference_values() { + let expected = [ + "18ded8d6-bcfc-e415-8539-944c0f5ea7a6", + "338c29e3-3024-4dbb-998d-7c04cf4fa88f", + "91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f", + "b2cf28af-b840-1071-3c6a-78085d8128b5", + "ea800387-ea1a-14e0-56cb-24f2022f969a", + "0af1ef7c-ad24-11dd-8790-001f5bf833e8", + "9d762b57-ffe3-2e34-d897-0c44c8e07c72", + "e12f6f01-8b0e-e00a-03c7-bc7e56a6cbdc", + "3e2984a2-f03c-71d5-3e97-75fb8c7e1e2f", + "29bbb16c-4b0c-4809-8de5-4b4df7cca8ef", + "e0f8b768-e68d-a0cc-d1d8-c2e7f3b51b6e", + ]; + assert_eq!( + baked_texture_hash(), + expected + .into_iter() + .map(|value| UUID::new_with_string(value.to_owned()).unwrap()) + .collect::>() + ); + assert!(AppearanceSetEventArgs::native_new(true).native_success()); + assert!(!AppearanceSetEventArgs::native_new(false).native_success()); + let texture_id = UUID::new_with_u_int64(700).unwrap(); + assert_eq!( + RebakeAvatarTexturesEventArgs::native_new(texture_id).native_texture_id(), + texture_id + ); + assert_eq!(wearable_type_from_u8(0), Some(WearableType::Shape)); + assert_eq!(wearable_type_from_u8(16), Some(WearableType::Universal)); + assert_eq!(wearable_type_from_u8(u8::MAX), Some(WearableType::Invalid)); + assert_eq!(wearable_type_from_u8(17), None); + } + + #[test] + fn appearance_packet_contains_all_bakes_visual_parameters_and_texture_entry() { + let client = GridClient::new().unwrap(); + let packet = client.appearance().native_make_appearance_packet().unwrap(); + assert_eq!(packet.wearable_data.len(), 11); + assert_eq!(packet.visual_param.len(), 218); + assert_eq!(packet.object_data.texture_entry.len(), 80); + assert_eq!( + &packet.object_data.texture_entry[..16], + default_avatar_texture().get_bytes().unwrap().as_slice() + ); + assert_eq!(packet.object_data.texture_entry[16], 0); + let snapshot = client.appearance(); + assert_eq!( + snapshot.my_visual_parameters.len(), + packet.visual_param.len() + ); + assert_eq!( + snapshot.my_visual_parameters, + packet + .visual_param + .iter() + .map(|block| block.param_value) + .collect::>() + ); + + let physics_client = GridClient::new().unwrap(); + let physics_manager = physics_client.appearance(); + write(&physics_manager.inner.wearables).push(WearableRecord { + item_id: UUID::new_with_u_int64(901).unwrap(), + asset_id: UUID::new_with_u_int64(902).unwrap(), + asset_type: AssetType::Clothing, + wearable_type: WearableType::Physics, + }); + let physics_packet = physics_manager.native_make_appearance_packet().unwrap(); + assert_eq!(physics_packet.visual_param.len(), 251); + } + + #[test] + fn texture_entry_encodes_face_bitfields_before_texture_ids() { + let texture_id = UUID::new_with_u_int64(808).unwrap(); + let mut textures = vec![UUID::zero(); 9]; + textures[8] = texture_id; + let encoded = encode_texture_entry(&textures).unwrap(); + assert_eq!( + &encoded[..16], + default_avatar_texture().get_bytes().unwrap().as_slice() + ); + assert_eq!(&encoded[16..18], &[0x82, 0x00]); + assert_eq!(&encoded[18..34], texture_id.get_bytes().unwrap().as_slice()); + assert_eq!(encoded[34], 0); + assert_eq!(encoded.len(), 98); + } + + #[test] + fn legacy_wearables_packet_preserves_valid_item_asset_and_type_fields() { + let client = GridClient::new().unwrap(); + let manager = client.appearance(); + manager.inner.busy.store(true, Ordering::Release); + let item_id = UUID::new_with_u_int64(1_001).unwrap(); + let asset_id = UUID::new_with_u_int64(1_002).unwrap(); + let mut packet = AgentWearablesUpdatePacket::new_with_constructor().unwrap(); + let mut shirt = + crate::packets::AgentWearablesUpdatePacketWearableDataBlock::new_with_constructor() + .unwrap(); + shirt.item_id = item_id; + shirt.asset_id = asset_id; + shirt.wearable_type = WearableType::Shirt as u8; + packet.wearable_data.push(shirt); + let mut empty = + crate::packets::AgentWearablesUpdatePacketWearableDataBlock::new_with_constructor() + .unwrap(); + empty.wearable_type = WearableType::Pants as u8; + packet.wearable_data.push(empty); + + handle_appearance_packet( + &manager.inner, + PacketType::AgentWearablesUpdate, + packet.to_bytes_with_method().unwrap(), + ); + + let wearables = read(&manager.inner.wearables); + assert_eq!(wearables.len(), 1); + assert_eq!(wearables[0].item_id, item_id); + assert_eq!(wearables[0].asset_id, asset_id); + assert_eq!(wearables[0].asset_type, AssetType::Clothing); + assert_eq!(wearables[0].wearable_type, WearableType::Shirt); + assert_eq!( + manager + .inner + .wearables_reply_generation + .load(Ordering::Acquire), + 1 + ); + } + + #[tokio::test] + async fn failed_appearance_request_emits_failure_and_empty_cache_request_does_not_wait() { + let client = GridClient::new().unwrap(); + let manager = client.appearance(); + assert!(!manager.native_request_cached_bakes_sent().unwrap()); + + let observed = Arc::new(Mutex::new(Vec::new())); + let callback_observed = Arc::clone(&observed); + let _subscription = manager.native_subscribe_appearance_set(Arc::new(move |event| { + mutex(&callback_observed).push(event.native_success()); + })); + + assert_eq!( + manager.native_request_set_appearance(false).await, + Err(Error::InvalidOperation) + ); + assert_eq!(*mutex(&observed), vec![false]); + assert!(!manager.native_manager_busy()); + } } diff --git a/crates/libremetaverse/src/asset_models.rs b/crates/libremetaverse/src/asset_models.rs index b3a5cac..faea994 100644 --- a/crates/libremetaverse/src/asset_models.rs +++ b/crates/libremetaverse/src/asset_models.rs @@ -464,6 +464,52 @@ pub struct AssetTexture { pub image: Option, } impl AssetTexture { + pub(crate) fn native_bytes(&self) -> Vec { + self.raw.bytes() + } + + pub(crate) fn native_deep_clone(&self) -> Result { + Ok(Self { + raw: RawAsset::with_data( + AssetType::Texture, + self.raw + .0 + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .asset_id(), + self.raw.bytes(), + )?, + components: self.components, + image: self.image.as_ref().map(ManagedImage::clone).transpose()?, + }) + } + + pub(crate) fn native_decode_image(&mut self) -> Result { + if self.image.is_some() { + return Ok(true); + } + #[cfg(feature = "jpeg2000")] + { + self.image = Some(libremetaverse_imaging::J2kCodec::decode_bytes( + &self.raw.bytes(), + libremetaverse_imaging::J2kDecodeOptions::default(), + )?); + self.components = if self.image.as_ref().is_some_and(|image| { + image + .channels + .contains(libremetaverse_imaging::ManagedImageImageChannels::ALPHA) + }) { + 4 + } else { + 3 + }; + Ok(true) + } + #[cfg(not(feature = "jpeg2000"))] + { + Err(Error::InvalidOperation) + } + } pub fn new_with_constructor() -> Result { Ok(Self { raw: RawAsset::empty(AssetType::Texture)?, @@ -557,6 +603,63 @@ impl AssetWearable { wearable_type: WearableType::Invalid, }) } + + pub(crate) fn native_from_asset( + asset: &Asset, + wearable_type: WearableType, + ) -> Result { + if !matches!( + asset.asset_type(), + AssetType::Bodypart | AssetType::Clothing + ) { + return Err(Error::Argument); + } + let text = std::str::from_utf8(&asset.asset_data).map_err(|_| Error::Argument)?; + let mut result = Self::empty(asset.asset_type())?; + result.raw = RawAsset::with_data( + asset.asset_type(), + asset.asset_id(), + asset.asset_data.clone(), + )?; + result.wearable_type = wearable_type; + let lines: Vec<_> = text.lines().collect(); + let mut index = 0; + while index < lines.len() { + let line = lines[index].trim(); + if let Some(count) = line + .strip_prefix("parameters ") + .and_then(|value| value.parse::().ok()) + { + for parameter in lines.iter().skip(index + 1).take(count) { + let mut values = parameter.split_whitespace(); + if let (Some(id), Some(value)) = (values.next(), values.next()) + && let (Ok(id), Ok(value)) = (id.parse::(), value.parse::()) + { + result.params.insert(id, value); + } + } + index = index.saturating_add(count); + } else if let Some(count) = line + .strip_prefix("textures ") + .and_then(|value| value.parse::().ok()) + { + for texture in lines.iter().skip(index + 1).take(count) { + let mut values = texture.split_whitespace(); + if let (Some(slot), Some(id)) = (values.next(), values.next()) + && let (Ok(slot), Ok(id)) = + (slot.parse::(), UUID::new_with_string(id.to_owned())) + && let Some(slot) = + crate::appearance_manager::avatar_texture_index_from_i32(slot) + { + result.textures.insert(slot, id); + } + } + index = index.saturating_add(count); + } + index += 1; + } + Ok(result) + } pub fn decode(&self) -> Result { Ok(std::str::from_utf8(&self.raw.bytes()).is_ok()) } @@ -675,3 +778,45 @@ impl AssetNotecard { AssetType::Notecard } } + +#[cfg(test)] +mod appearance_asset_tests { + use super::*; + + #[test] + fn wearable_parser_preserves_parameters_and_texture_indices() { + let texture_id = UUID::new_with_u_int64(42).unwrap(); + let bytes = format!( + "LLWearable version 22\nshirt\ndescription\ntype 4\nparameters 2\n33 0.75\n108 -0.25\ntextures 2\n1 {texture_id}\n13 {texture_id}\n" + ) + .into_bytes(); + let asset_id = UUID::new_with_u_int64(41).unwrap(); + let asset = Asset::native_new(AssetType::Clothing, asset_id, bytes).unwrap(); + let wearable = AssetWearable::native_from_asset(&asset, WearableType::Shirt).unwrap(); + assert_eq!(wearable.params.get(&33), Some(&0.75)); + assert_eq!(wearable.params.get(&108), Some(&-0.25)); + assert_eq!( + wearable + .textures + .get(&crate::AvatarTextureIndex::UpperShirt), + Some(&texture_id) + ); + assert_eq!( + wearable + .textures + .get(&crate::AvatarTextureIndex::UpperJacket), + Some(&texture_id) + ); + assert_eq!(wearable.wearable_type, WearableType::Shirt); + assert_eq!(wearable.raw.0.read().unwrap().asset_id(), asset_id); + } + + #[test] + fn wearable_parser_rejects_non_wearable_assets() { + let asset = Asset::native_new(AssetType::Texture, UUID::zero(), Vec::new()).unwrap(); + assert!(matches!( + AssetWearable::native_from_asset(&asset, WearableType::Shirt), + Err(Error::Argument) + )); + } +} diff --git a/crates/libremetaverse/src/baking_texture_provider.rs b/crates/libremetaverse/src/baking_texture_provider.rs new file mode 100644 index 0000000..8d59254 --- /dev/null +++ b/crates/libremetaverse/src/baking_texture_provider.rs @@ -0,0 +1,56 @@ +//! Default decoded-texture provider for the avatar baking pipeline. + +use crate::client_core::ClientWeakHandle; +use crate::{Error, GridClient, IBakingTextureProvider}; +use libremetaverse_types::UUID; +use libremetaverse_types::compat::CancellationToken; +use std::future::Future; +use std::pin::Pin; + +pub struct GridClientBakingTextureProvider { + client: ClientWeakHandle, +} + +impl GridClientBakingTextureProvider { + #[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] + // The generated constructor owns its GridClient argument and uses the shared Result model. + pub(crate) fn native_new(client: GridClient) -> Result { + Ok(Self { + client: client.native_weak_handle(), + }) + } + + pub(crate) async fn native_request_texture( + &self, + texture_id: UUID, + cancellation_token: Option, + ) -> Result, Error> { + let token = cancellation_token.unwrap_or_default(); + token.throw_if_cancellation_requested()?; + let client = self.client.upgrade().ok_or(Error::InvalidOperation)?; + let Some(mut texture) = client + .native_assets()? + .request_image(texture_id, None, Some(token.clone())) + .await? + else { + return Ok(None); + }; + token.throw_if_cancellation_requested()?; + if texture.native_decode_image()? { + Ok(Some(texture)) + } else { + Ok(None) + } + } +} + +impl IBakingTextureProvider for GridClientBakingTextureProvider { + fn request_texture( + &self, + texture_id: UUID, + cancellation_token: Option, + ) -> Pin, Error>> + Send + '_>> + { + Box::pin(self.native_request_texture(texture_id, cancellation_token)) + } +} diff --git a/crates/libremetaverse/src/generated.rs b/crates/libremetaverse/src/generated.rs index 5e6246c..d25aa7c 100644 --- a/crates/libremetaverse/src/generated.rs +++ b/crates/libremetaverse/src/generated.rs @@ -2888,9 +2888,7 @@ impl AppearanceManager { libremetaverse::AgentCachedBakesReplyEventArgs, >, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!( - "E:LibreMetaverse.AppearanceManager.CachedBakesReply" - ) + self.native_subscribe_cached_bakes_reply(handler) } /// C# member: `E:LibreMetaverse.AppearanceManager.RebakeAvatarRequested`. pub fn subscribe_rebake_avatar_requested( @@ -2899,118 +2897,82 @@ impl AppearanceManager { libremetaverse::RebakeAvatarTexturesEventArgs, >, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!( - "E:LibreMetaverse.AppearanceManager.RebakeAvatarRequested" - ) + self.native_subscribe_rebake_avatar_requested(handler) } /// C# member: `F:LibreMetaverse.AppearanceManager.ATTACHMENT_ADD`. pub fn attachment_add() -> u8 { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.ATTACHMENT_ADD" - ) + crate::appearance_manager::ATTACHMENT_ADD } /// C# member: `F:LibreMetaverse.AppearanceManager.BAKED_TEXTURE_COUNT`. pub const BAKED_TEXTURE_COUNT: i32 = 11; /// C# member: `F:LibreMetaverse.AppearanceManager.BAKED_TEXTURE_HASH`. pub fn baked_texture_hash() -> Vec { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.BAKED_TEXTURE_HASH" - ) + crate::appearance_manager::baked_texture_hash() } /// C# member: `F:LibreMetaverse.AppearanceManager.BakeIndexToTextureIndex`. pub fn bake_index_to_texture_index() -> Vec { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.BakeIndexToTextureIndex" - ) + crate::appearance_manager::bake_index_to_texture_index() } /// C# member: `F:LibreMetaverse.AppearanceManager.DEFAULT_AVATAR_TEXTURE`. pub fn default_avatar_texture() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.DEFAULT_AVATAR_TEXTURE" - ) + crate::appearance_manager::default_avatar_texture() } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX1`. pub fn img_use_baked_aux1() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX1" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::BakedAux1) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX2`. pub fn img_use_baked_aux2() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX2" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::BakedAux2) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX3`. pub fn img_use_baked_aux3() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX3" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::BakedAux3) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_EYES`. pub fn img_use_baked_eyes() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_EYES" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::Eyes) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_HAIR`. pub fn img_use_baked_hair() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_HAIR" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::Hair) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_HEAD`. pub fn img_use_baked_head() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_HEAD" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::Head) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_INDICES`. pub fn img_use_baked_indices() -> libremetaverse_types::compat::FrozenDictionary< libremetaverse_types::UUID, libremetaverse::AvatarTextureIndex, > { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_INDICES" - ) + crate::appearance_manager::img_use_baked_indices() } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LEFTARM`. pub fn img_use_baked_leftarm() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LEFTARM" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::BakedLeftArm) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LEFTLEG`. pub fn img_use_baked_leftleg() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LEFTLEG" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::BakedLeftLeg) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LOWER`. pub fn img_use_baked_lower() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LOWER" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::LowerBody) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_SKIRT`. pub fn img_use_baked_skirt() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_SKIRT" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::Skirt) } /// C# member: `F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_UPPER`. pub fn img_use_baked_upper() -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_UPPER" - ) + crate::appearance_manager::img_use_baked(crate::BakeType::UpperBody) } /// C# member: `F:LibreMetaverse.AppearanceManager.WEARABLES_PER_LAYER`. pub const WEARABLES_PER_LAYER: i32 = 9; /// C# member: `F:LibreMetaverse.AppearanceManager.WEARABLE_BAKE_MAP`. pub fn wearable_bake_map() -> Vec> { - libremetaverse_types::unimplemented_api!( - "F:LibreMetaverse.AppearanceManager.WEARABLE_BAKE_MAP" - ) + crate::appearance_manager::wearable_bake_map() } /// C# member: `F:LibreMetaverse.AppearanceManager.WEARABLE_COUNT`. pub const WEARABLE_COUNT: i32 = 16; @@ -3092,17 +3054,15 @@ impl AppearanceManager { pub fn bake_type_to_agent_texture_index( index: libremetaverse::BakeType, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.BakeTypeToAgentTextureIndex(LibreMetaverse.BakeType)", - ) + Ok(crate::appearance_manager::bake_type_to_agent_texture_index( + index, + )) } /// C# member: `M:LibreMetaverse.AppearanceManager.BakeTypeToTextures(LibreMetaverse.BakeType)`. pub fn bake_type_to_textures( bake_type: libremetaverse::BakeType, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.BakeTypeToTextures(LibreMetaverse.BakeType)", - ) + Ok(crate::appearance_manager::bake_type_to_textures(bake_type)) } /// C# member: `M:LibreMetaverse.AppearanceManager.BlendToArchetype(LibreMetaverse.GenepoolArchetype,System.Single)`. pub async fn blend_to_archetype( @@ -3119,9 +3079,7 @@ impl AppearanceManager { wearable: libremetaverse::AppearanceManagerWearableData, textures: &mut Vec, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.DecodeWearableParams(LibreMetaverse.AppearanceManager.WearableData,LibreMetaverse.AppearanceManager.TextureData[]@)", - ) + crate::appearance_manager::decode_wearable_params(wearable, textures) } /// C# member: `M:LibreMetaverse.AppearanceManager.Detach(LibreMetaverse.InventoryItem)`. pub fn detach_with_inventory_item( @@ -3189,9 +3147,7 @@ impl AppearanceManager { pub fn get_color_from_params( param: Vec, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.GetColorFromParams(System.Collections.Generic.List{LibreMetaverse.AppearanceManager.ColorParamInfo})", - ) + crate::appearance_manager::color_from_params(param) } /// C# member: `M:LibreMetaverse.AppearanceManager.GetCurrentOutfitFolderAsync(System.Threading.CancellationToken)`. pub async fn get_current_outfit_folder( @@ -3205,9 +3161,7 @@ impl AppearanceManager { pub fn get_current_param_values( &self, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.GetCurrentParamValues", - ) + self.native_get_current_param_values() } /// C# member: `M:LibreMetaverse.AppearanceManager.GetFolderWearablesAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`. pub async fn get_folder_wearables( @@ -3269,17 +3223,15 @@ impl AppearanceManager { pub fn make_appearance_packet( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.MakeAppearancePacket", - ) + self.native_make_appearance_packet() } /// C# member: `M:LibreMetaverse.AppearanceManager.MorphLayerForBakeType(LibreMetaverse.BakeType)`. pub fn morph_layer_for_bake_type( bake_type: libremetaverse::BakeType, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.MorphLayerForBakeType(LibreMetaverse.BakeType)", - ) + Ok(crate::appearance_manager::morph_layer_for_bake_type( + bake_type, + )) } /// C# member: `M:LibreMetaverse.AppearanceManager.RandomizeAppearance(System.Random)`. pub async fn randomize_appearance( @@ -3328,18 +3280,15 @@ impl AppearanceManager { } /// C# member: `M:LibreMetaverse.AppearanceManager.RequestCachedBakes`. pub fn request_cached_bakes(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.RequestCachedBakes", - ) + self.native_request_cached_bakes() } /// C# member: `M:LibreMetaverse.AppearanceManager.RequestSetAppearance(System.Boolean)`. pub async fn request_set_appearance( &self, force_rebake: Option, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.RequestSetAppearance(System.Boolean)", - ) + self.native_request_set_appearance(force_rebake.unwrap_or(false)) + .await } /// C# member: `M:LibreMetaverse.AppearanceManager.SendOutfitToCurrentSimulatorAsync(System.Threading.CancellationToken)`. pub async fn send_outfit_to_current_simulator( @@ -3350,9 +3299,7 @@ impl AppearanceManager { } /// C# member: `M:LibreMetaverse.AppearanceManager.ServerBakingRegion`. pub fn server_baking_region(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.ServerBakingRegion", - ) + self.native_server_baking_region() } /// C# member: `M:LibreMetaverse.AppearanceManager.UpdateLastReceivedCOFVersion(System.Int32)`. pub fn update_last_received_cof_version( @@ -3401,18 +3348,14 @@ impl AppearanceManager { } /// C# member: `P:LibreMetaverse.AppearanceManager.TextureProvider`. pub fn texture_provider(&self) -> std::sync::Arc { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.AppearanceManager.TextureProvider" - ) + self.native_texture_provider() } /// Setter for C# member: `P:LibreMetaverse.AppearanceManager.TextureProvider`. pub fn set_texture_provider( &mut self, value: std::sync::Arc, ) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.AppearanceManager.TextureProvider" - ) + self.native_set_texture_provider(value) } } @@ -3445,15 +3388,11 @@ pub struct AppearanceManagerTextureData { impl AppearanceManagerTextureData { /// C# member: `M:LibreMetaverse.AppearanceManager.TextureData.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.TextureData.#ctor", - ) + crate::appearance_manager::texture_data_defaults() } /// C# member: `M:LibreMetaverse.AppearanceManager.TextureData.ToString`. pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.AppearanceManager.TextureData.ToString" - ) + crate::appearance_manager::texture_data_to_string(self) } } @@ -3474,15 +3413,11 @@ pub struct AppearanceManagerWearableData { impl AppearanceManagerWearableData { /// C# member: `M:LibreMetaverse.AppearanceManager.WearableData.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceManager.WearableData.#ctor", - ) + crate::appearance_manager::wearable_data_defaults() } /// C# member: `M:LibreMetaverse.AppearanceManager.WearableData.ToString`. pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.AppearanceManager.WearableData.ToString" - ) + crate::appearance_manager::wearable_data_to_string(self) } } @@ -3498,17 +3433,15 @@ impl AppearanceManagerException { } /// C# type: `T:LibreMetaverse.AppearanceSetEventArgs`. -pub struct AppearanceSetEventArgs; +pub use crate::appearance_manager::AppearanceSetEventArgs; impl AppearanceSetEventArgs { /// C# member: `M:LibreMetaverse.AppearanceSetEventArgs.#ctor(System.Boolean)`. pub fn new(success: bool) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AppearanceSetEventArgs.#ctor(System.Boolean)", - ) + Ok(crate::appearance_manager::AppearanceSetEventArgs::native_new(success)) } /// C# member: `P:LibreMetaverse.AppearanceSetEventArgs.Success`. pub fn success(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AppearanceSetEventArgs.Success") + self.native_success() } } @@ -8948,13 +8881,11 @@ impl GridClient { } /// C# type: `T:LibreMetaverse.GridClientBakingTextureProvider`. -pub struct GridClientBakingTextureProvider; +pub use crate::baking_texture_provider::GridClientBakingTextureProvider; impl GridClientBakingTextureProvider { /// C# member: `M:LibreMetaverse.GridClientBakingTextureProvider.#ctor(LibreMetaverse.GridClient)`. pub fn new(client: libremetaverse::GridClient) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.GridClientBakingTextureProvider.#ctor(LibreMetaverse.GridClient)", - ) + crate::baking_texture_provider::GridClientBakingTextureProvider::native_new(client) } /// C# member: `M:LibreMetaverse.GridClientBakingTextureProvider.RequestTextureAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`. pub async fn request_texture( @@ -8962,12 +8893,10 @@ impl GridClientBakingTextureProvider { texture_id: libremetaverse_types::UUID, cancellation_token: Option, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.GridClientBakingTextureProvider.RequestTextureAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)", - ) + self.native_request_texture(texture_id, cancellation_token) + .await } } -impl libremetaverse::IBakingTextureProvider for GridClientBakingTextureProvider {} /// C# type: `T:LibreMetaverse.GridClientServiceCollectionExtensions`. pub struct GridClientServiceCollectionExtensions; @@ -10779,7 +10708,7 @@ pub use crate::caps_http::HttpCapsClient; pub use crate::caps_http::HttpCapsClientProgressReport; /// C# type: `T:LibreMetaverse.IBakingTextureProvider`. -pub trait IBakingTextureProvider: std::any::Any { +pub trait IBakingTextureProvider: std::any::Any + Send + Sync { /// C# member: `M:LibreMetaverse.IBakingTextureProvider.RequestTextureAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`. fn request_texture( &self, @@ -19873,19 +19802,15 @@ impl ProtocolManager { } /// C# type: `T:LibreMetaverse.RebakeAvatarTexturesEventArgs`. -pub struct RebakeAvatarTexturesEventArgs; +pub use crate::appearance_manager::RebakeAvatarTexturesEventArgs; impl RebakeAvatarTexturesEventArgs { /// C# member: `M:LibreMetaverse.RebakeAvatarTexturesEventArgs.#ctor(LibreMetaverse.UUID)`. pub fn new(texture_id: libremetaverse_types::UUID) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RebakeAvatarTexturesEventArgs.#ctor(LibreMetaverse.UUID)", - ) + Ok(crate::appearance_manager::RebakeAvatarTexturesEventArgs::native_new(texture_id)) } /// C# member: `P:LibreMetaverse.RebakeAvatarTexturesEventArgs.TextureID`. pub fn texture_id(&self) -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RebakeAvatarTexturesEventArgs.TextureID" - ) + self.native_texture_id() } } @@ -23290,25 +23215,21 @@ pub mod appearance { } /// C# type: `T:LibreMetaverse.Appearance.InitialOutfit`. - pub struct InitialOutfit; + pub use crate::initial_outfit::InitialOutfit; impl InitialOutfit { /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.#ctor(LibreMetaverse.GridClient,LibreMetaverse.Appearance.CurrentOutfitFolder)`. pub fn new( client: libremetaverse::GridClient, cof: libremetaverse::appearance::CurrentOutfitFolder, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.#ctor(LibreMetaverse.GridClient,LibreMetaverse.Appearance.CurrentOutfitFolder)", - ) + crate::initial_outfit::InitialOutfit::native_new(client, cof) } /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.CheckSystemFoldersAsync(System.Threading.CancellationToken)`. pub async fn check_system_folders( &self, cancellation_token: Option, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.CheckSystemFoldersAsync(System.Threading.CancellationToken)", - ) + self.native_check_system_folders(cancellation_token).await } /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.CopyFolderAsync(LibreMetaverse.InventoryFolder,LibreMetaverse.UUID,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})`. pub async fn copy_folder( @@ -23324,9 +23245,8 @@ pub mod appearance { >, >, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.CopyFolderAsync(LibreMetaverse.InventoryFolder,LibreMetaverse.UUID,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})", - ) + self.native_copy_folder(folder, destination, cancellation_token, progress) + .await } /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.CreateFolderAsync(LibreMetaverse.UUID,System.String,LibreMetaverse.FolderType,System.Threading.CancellationToken)`. pub async fn create_folder( @@ -23336,18 +23256,15 @@ pub mod appearance { type_: libremetaverse_types::FolderType, cancellation_token: Option, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.CreateFolderAsync(LibreMetaverse.UUID,System.String,LibreMetaverse.FolderType,System.Threading.CancellationToken)", - ) + self.native_create_folder(parent, name, type_, cancellation_token) + .await } /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.FindNodeByName(LibreMetaverse.InventoryNode,System.String)`. pub fn find_node_by_name( root: libremetaverse::InventoryNode, name: String, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.FindNodeByName(LibreMetaverse.InventoryNode,System.String)", - ) + crate::initial_outfit::InitialOutfit::native_find_node_by_name(root, name) } /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.SetInitialOutfitAsync(System.String,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})`. pub async fn set_initial_outfit( @@ -23362,9 +23279,8 @@ pub mod appearance { >, >, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.SetInitialOutfitAsync(System.String,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})", - ) + self.native_set_initial_outfit(outfit, cancellation_token, progress) + .await } } @@ -23383,81 +23299,59 @@ pub mod appearance { } /// C# type: `T:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress`. - pub struct InitialOutfitInitialOutfitProgress; + pub use crate::initial_outfit::InitialOutfitProgress as InitialOutfitInitialOutfitProgress; impl InitialOutfitInitialOutfitProgress { /// C# member: `M:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.#ctor", - ) + crate::initial_outfit::InitialOutfitProgress::native_new() } /// C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.CurrentItemName`. pub fn current_item_name(&self) -> String { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.CurrentItemName" - ) + self.native_current_item_name() } /// Setter for C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.CurrentItemName`. pub fn set_current_item_name(&mut self, value: String) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.CurrentItemName" - ) + self.native_set_current_item_name(value) } /// C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.ItemsCopied`. pub fn items_copied(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.ItemsCopied" - ) + self.native_items_copied() } /// Setter for C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.ItemsCopied`. pub fn set_items_copied(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.ItemsCopied" - ) + self.native_set_items_copied(value) } /// C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Message`. pub fn message(&self) -> String { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Message" - ) + self.native_message() } /// Setter for C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Message`. pub fn set_message(&mut self, value: String) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Message" - ) + self.native_set_message(value) } /// C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Phase`. pub fn phase(&self) -> libremetaverse::appearance::InitialOutfitInitialOutfitPhase { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Phase" - ) + self.native_phase() } /// Setter for C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Phase`. pub fn set_phase( &mut self, value: libremetaverse::appearance::InitialOutfitInitialOutfitPhase, ) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Phase" - ) + self.native_set_phase(value) } /// C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.TotalItems`. pub fn total_items(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.TotalItems" - ) + self.native_total_items() } /// Setter for C# member: `P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.TotalItems`. pub fn set_total_items(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.TotalItems" - ) + self.native_set_total_items(value) } } /// C# type: `T:LibreMetaverse.Appearance.InitialOutfitHandler`. - pub struct InitialOutfitHandler; + pub use crate::initial_outfit::InitialOutfitHandler; impl InitialOutfitHandler { /// C# member: `M:LibreMetaverse.Appearance.InitialOutfitHandler.#ctor(LibreMetaverse.GridClient,LibreMetaverse.Appearance.CurrentOutfitFolder,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})`. pub fn new( @@ -23471,15 +23365,11 @@ pub mod appearance { >, >, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfitHandler.#ctor(LibreMetaverse.GridClient,LibreMetaverse.Appearance.CurrentOutfitFolder,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})", - ) + crate::initial_outfit::InitialOutfitHandler::native_new(client, cof, progress) } /// C# member: `M:LibreMetaverse.Appearance.InitialOutfitHandler.Dispose`. pub fn dispose(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Appearance.InitialOutfitHandler.Dispose", - ) + self.native_dispose() } } } @@ -26647,62 +26537,54 @@ pub mod http { pub mod imaging { /// C# type: `T:LibreMetaverse.Imaging.Baker`. - pub struct Baker; + pub use crate::appearance_baker::Baker; impl Baker { /// C# member: `M:LibreMetaverse.Imaging.Baker.#ctor(LibreMetaverse.BakeType)`. pub fn new(bake_type: libremetaverse::BakeType) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Baker.#ctor(LibreMetaverse.BakeType)", - ) + crate::appearance_baker::Baker::native_new(bake_type) } /// C# member: `M:LibreMetaverse.Imaging.Baker.AddTexture(LibreMetaverse.AppearanceManager.TextureData)`. pub fn add_texture( &mut self, tdata: libremetaverse::AppearanceManagerTextureData, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Baker.AddTexture(LibreMetaverse.AppearanceManager.TextureData)", - ) + self.native_add_texture(tdata) } /// C# member: `M:LibreMetaverse.Imaging.Baker.Bake`. pub fn bake(&mut self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Imaging.Baker.Bake") + self.native_bake() } /// C# member: `M:LibreMetaverse.Imaging.Baker.BakeTypeFor(LibreMetaverse.AvatarTextureIndex)`. pub fn bake_type_for( index: libremetaverse::AvatarTextureIndex, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Baker.BakeTypeFor(LibreMetaverse.AvatarTextureIndex)", - ) + Ok(crate::appearance_baker::Baker::native_bake_type_for(index)) } /// C# member: `M:LibreMetaverse.Imaging.Baker.LoadResourceLayer(System.String)`. pub fn load_resource_layer( file_name: String, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Baker.LoadResourceLayer(System.String)", - ) + crate::appearance_baker::Baker::native_load_resource_layer(file_name) } /// C# member: `P:LibreMetaverse.Imaging.Baker.BakeHeight`. pub fn bake_height(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Imaging.Baker.BakeHeight") + self.native_bake_height() } /// C# member: `P:LibreMetaverse.Imaging.Baker.BakeType`. pub fn bake_type(&self) -> libremetaverse::BakeType { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Imaging.Baker.BakeType") + self.native_bake_type() } /// C# member: `P:LibreMetaverse.Imaging.Baker.BakeWidth`. pub fn bake_width(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Imaging.Baker.BakeWidth") + self.native_bake_width() } /// C# member: `P:LibreMetaverse.Imaging.Baker.BakedTexture`. pub fn baked_texture(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Imaging.Baker.BakedTexture") + self.native_baked_texture() } /// C# member: `P:LibreMetaverse.Imaging.Baker.Textures`. pub fn textures(&self) -> Vec { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Imaging.Baker.Textures") + self.native_textures() } } diff --git a/crates/libremetaverse/src/initial_outfit.rs b/crates/libremetaverse/src/initial_outfit.rs new file mode 100644 index 0000000..0a6b05f --- /dev/null +++ b/crates/libremetaverse/src/initial_outfit.rs @@ -0,0 +1,607 @@ +//! Transactional first-login outfit copy and application. + +use crate::appearance::{CurrentOutfitFolder, InitialOutfitInitialOutfitPhase as Phase}; +use crate::{ + Error, GridClient, InventoryBase, InventoryFolder, InventoryNode, InventorySortOrder, + LoginStatus, +}; +use libremetaverse_types::compat::{ + CancellationToken, CancellationTokenSource, IProgress, Subscription, +}; +use libremetaverse_types::{FolderType, UUID}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +const MAX_FOLDERS: usize = 256; + +#[derive(Clone, Debug)] +pub struct InitialOutfitProgress { + current_item_name: String, + items_copied: i32, + message: String, + phase: Phase, + total_items: i32, +} + +impl InitialOutfitProgress { + #[allow(clippy::unnecessary_wraps)] // The generated constructor uses the crate Result model. + pub(crate) fn native_new() -> Result { + Ok(Self { + current_item_name: String::new(), + items_copied: 0, + message: String::new(), + phase: Phase::Counting, + total_items: 0, + }) + } + pub(crate) fn native_current_item_name(&self) -> String { + self.current_item_name.clone() + } + pub(crate) fn native_set_current_item_name(&mut self, value: String) { + self.current_item_name = value; + } + pub(crate) const fn native_items_copied(&self) -> i32 { + self.items_copied + } + pub(crate) fn native_set_items_copied(&mut self, value: i32) { + self.items_copied = value; + } + pub(crate) fn native_message(&self) -> String { + self.message.clone() + } + pub(crate) fn native_set_message(&mut self, value: String) { + self.message = value; + } + pub(crate) const fn native_phase(&self) -> Phase { + self.phase + } + pub(crate) fn native_set_phase(&mut self, value: Phase) { + self.phase = value; + } + pub(crate) const fn native_total_items(&self) -> i32 { + self.total_items + } + pub(crate) fn native_set_total_items(&mut self, value: i32) { + self.total_items = value; + } +} + +#[derive(Clone)] +pub struct InitialOutfit { + client: GridClient, + cof: CurrentOutfitFolder, +} + +struct FolderPlan { + source: InventoryFolder, + children: Vec, + items: Vec, +} + +impl InitialOutfit { + #[allow(clippy::unnecessary_wraps)] // The generated constructor uses the crate Result model. + pub(crate) fn native_new(client: GridClient, cof: CurrentOutfitFolder) -> Result { + Ok(Self { client, cof }) + } + + #[allow(clippy::needless_pass_by_value)] // Mirrors the public String signature. + pub(crate) fn native_find_node_by_name( + root: InventoryNode, + name: String, + ) -> Result, Error> { + if name.is_empty() { + return Ok(None); + } + let mut queue = VecDeque::from([root]); + let mut visited = HashSet::new(); + let mut traversed = 0_usize; + while let Some(node) = queue.pop_front() { + traversed += 1; + if traversed > MAX_FOLDERS { + return Err(Error::InvalidOperation); + } + let identity = node.data().map(|value| value.inventory_base().uuid()); + if let Some(identity) = identity + && !visited.insert(identity) + { + continue; + } + if visited.len() > MAX_FOLDERS { + return Err(Error::InvalidOperation); + } + if node + .data() + .is_some_and(|value| value.inventory_base().name() == name) + { + return Ok(Some(node)); + } + queue.extend(node.nodes().values()); + } + Ok(None) + } + + #[allow(clippy::unused_async)] // Preserves the Task-returning compatibility API. + pub(crate) async fn native_create_folder( + &self, + parent: UUID, + name: String, + folder_type: FolderType, + cancellation_token: Option, + ) -> Result { + let token = cancellation_token.unwrap_or_default(); + token.throw_if_cancellation_requested()?; + let id = self + .client + .native_inventory()? + .native_create_folder(parent, name, folder_type)?; + token.throw_if_cancellation_requested()?; + if id == UUID::zero() { + Err(Error::InvalidOperation) + } else { + Ok(id) + } + } + + pub(crate) async fn native_check_system_folders( + &self, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = cancellation_token.unwrap_or_default(); + token.throw_if_cancellation_requested()?; + let inventory = self.client.native_inventory()?; + let store = inventory.native_store().ok_or(Error::InvalidOperation)?; + let root = store + .root_folder() + .ok_or(Error::InvalidOperation)? + .base + .uuid(); + for (folder_type, name) in [ + (FolderType::Clothing, "Clothing"), + (FolderType::Trash, "Trash"), + ] { + token.throw_if_cancellation_requested()?; + if inventory.native_find_folder_for_type(folder_type) == root { + self.native_create_folder(root, name.to_owned(), folder_type, Some(token.clone())) + .await?; + } + } + Ok(()) + } + + async fn plan_copy( + &self, + root: InventoryFolder, + token: &CancellationToken, + ) -> Result<(Vec, i32), Error> { + let inventory = self.client.native_inventory()?; + let mut queue = VecDeque::from([root]); + let mut visited = HashSet::new(); + let mut plans = Vec::new(); + let mut total = 0_i32; + while let Some(folder) = queue.pop_front() { + token.throw_if_cancellation_requested()?; + if !visited.insert(folder.base.uuid()) { + continue; + } + if visited.len() > MAX_FOLDERS { + return Err(Error::InvalidOperation); + } + let contents = inventory + .native_folder_contents( + folder.base.uuid(), + folder.base.owner_id(), + true, + true, + InventorySortOrder::BY_NAME, + Some(token.clone()), + false, + ) + .await?; + let mut children = Vec::new(); + let mut items = Vec::new(); + for value in contents { + if let Some(child) = value.as_any().downcast_ref::() { + children.push(child.clone()); + queue.push_back(child.clone()); + } else { + total = total.checked_add(1).ok_or(Error::Argument)?; + items.push(value.inventory_base().clone()); + } + } + plans.push(FolderPlan { + source: folder, + children, + items, + }); + } + Ok((plans, total)) + } + + pub(crate) async fn native_copy_folder( + &self, + folder: InventoryFolder, + destination: UUID, + cancellation_token: Option, + progress: Option>>, + ) -> Result { + self.native_copy_folder_impl(folder, destination, cancellation_token, progress, true) + .await + .map(|(folder, _)| folder) + } + + async fn native_copy_folder_impl( + &self, + folder: InventoryFolder, + destination: UUID, + cancellation_token: Option, + progress: Option>>, + report_complete: bool, + ) -> Result<(UUID, i32), Error> { + let token = cancellation_token.unwrap_or_default(); + let (plans, total) = self.plan_copy(folder.clone(), &token).await?; + report( + progress.as_deref(), + Phase::Counting, + total, + 0, + "", + "Inventory counted", + )?; + let root_id = self + .native_create_folder( + destination, + folder.base.name(), + folder.preferred_type(), + Some(token.clone()), + ) + .await?; + let result = self + .execute_copy(&plans, root_id, total, &token, progress.as_deref()) + .await; + if let Err(error) = result { + self.cleanup_folder(root_id).await; + return Err(error); + } + if report_complete { + report( + progress.as_deref(), + Phase::Complete, + total, + total, + "", + "Folder copy complete", + )?; + } + Ok((root_id, total)) + } + + async fn cleanup_folder(&self, root_id: UUID) { + // Compensation deliberately ignores the cancelled caller token. + let Ok(inventory) = self.client.native_inventory() else { + return; + }; + let _ = inventory + .native_remove_inventory_objects(Vec::new(), vec![root_id], None) + .await; + if let Some(store) = inventory.native_store() + && let Ok(Some(folder)) = + store.get_value_or_default_with_uuid_a7c63fbe::(root_id) + { + let _ = store.remove_node_for(&folder); + } + } + + async fn execute_copy( + &self, + plans: &[FolderPlan], + root_id: UUID, + total: i32, + token: &CancellationToken, + progress: Option<&dyn IProgress>, + ) -> Result<(), Error> { + let inventory = self.client.native_inventory()?; + let root_source = plans.first().ok_or(Error::Argument)?.source.base.uuid(); + let mut destinations = HashMap::from([(root_source, root_id)]); + let mut copied = 0_i32; + for plan in plans { + token.throw_if_cancellation_requested()?; + let destination = *destinations + .get(&plan.source.base.uuid()) + .ok_or(Error::InvalidOperation)?; + for child in &plan.children { + let id = self + .native_create_folder( + destination, + child.base.name(), + child.preferred_type(), + Some(token.clone()), + ) + .await?; + destinations.insert(child.base.uuid(), id); + } + for item in &plan.items { + token.throw_if_cancellation_requested()?; + report( + progress, + Phase::Copying, + total, + copied, + &item.name(), + "Copying inventory item", + )?; + let copied_item = inventory + .native_copy_item( + item.uuid(), + destination, + item.name(), + item.owner_id(), + Some(token.clone()), + ) + .await?; + if copied_item.is_none() { + return Err(Error::InvalidOperation); + } + copied += 1; + report( + progress, + Phase::Copying, + total, + copied, + &item.name(), + "Inventory item copied", + )?; + } + } + Ok(()) + } + + pub(crate) async fn native_set_initial_outfit( + &self, + outfit: String, + cancellation_token: Option, + progress: Option>>, + ) -> Result<(), Error> { + if outfit.is_empty() { + return Err(Error::Argument); + } + let token = cancellation_token.unwrap_or_default(); + token.throw_if_cancellation_requested()?; + let inventory = self.client.native_inventory()?; + let store = inventory.native_store().ok_or(Error::InvalidOperation)?; + let node = Self::native_find_node_by_name(store.library_root_node(), outfit)? + .ok_or(Error::InvalidOperation)?; + let folder = node + .data() + .and_then(|value| value.as_any().downcast_ref::().cloned()) + .ok_or(Error::InvalidOperation)?; + self.native_check_system_folders(Some(token.clone())) + .await?; + let clothing = inventory.native_find_folder_for_type(FolderType::Clothing); + let wrapper = progress.map(Arc::>::from); + let (copied, total) = self + .native_copy_folder_impl( + folder, + clothing, + Some(token.clone()), + wrapper + .as_ref() + .map(|sink| Box::new(SharedProgress(Arc::clone(sink))) as Box<_>), + false, + ) + .await?; + report( + wrapper.as_deref(), + Phase::Applying, + total, + total, + "", + "Applying initial outfit", + )?; + let applied = async { + token.throw_if_cancellation_requested()?; + if self + .cof + .native_replace_outfit(copied, Some(token.clone())) + .await? + { + Ok(()) + } else { + Err(Error::InvalidOperation) + } + } + .await; + if let Err(error) = applied { + self.cleanup_folder(copied).await; + return Err(error); + } + report( + wrapper.as_deref(), + Phase::Complete, + total, + total, + "", + "Initial outfit applied", + ) + } +} + +#[allow(clippy::unnecessary_wraps)] // Keeps progress reporting composable with fallible stages. +fn report( + sink: Option<&dyn IProgress>, + phase: Phase, + total: i32, + copied: i32, + current: &str, + message: &str, +) -> Result<(), Error> { + if let Some(sink) = sink { + sink.report(InitialOutfitProgress { + current_item_name: current.to_owned(), + items_copied: copied, + message: message.to_owned(), + phase, + total_items: total, + }); + } + Ok(()) +} + +struct SharedProgress(Arc>); +impl IProgress for SharedProgress { + fn report(&self, value: InitialOutfitProgress) { + self.0.report(value); + } +} + +pub struct InitialOutfitHandler { + cancellation: CancellationTokenSource, + subscription: Mutex>, +} + +impl InitialOutfitHandler { + pub(crate) fn native_new( + client: GridClient, + cof: CurrentOutfitFolder, + progress: Option>>, + ) -> Result { + let cancellation = CancellationTokenSource::new(); + let token = cancellation.token(); + let started = Arc::new(AtomicBool::new(false)); + let callback_started = Arc::clone(&started); + let progress = progress.map(Arc::>::from); + let network = client.native_network()?; + let subscription = network.native_subscribe_login_progress(Arc::new(move |event| { + if event.status() != LoginStatus::Success { + return; + } + let Some(login) = client + .native_network() + .ok() + .and_then(|network| network.login_response_data) + else { + return; + }; + if !login.first_login() || login.initial_outfit().is_empty() { + return; + } + if callback_started + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return; + } + let client = client.clone(); + let cof = cof.clone(); + let outfit = login.initial_outfit(); + let token = token.clone(); + let progress = progress.as_ref().map(Arc::clone); + let _ = std::thread::Builder::new() + .name("initial-outfit".to_owned()) + .spawn(move || { + let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + else { + return; + }; + let mut client = client; + let _ = runtime.block_on(async move { + let _ = client + .native_self() + .native_set_agent_access_async("A".to_owned(), Some(token.clone())) + .await; + let initial = InitialOutfit { client, cof }; + initial + .native_set_initial_outfit( + outfit, + Some(token), + progress.map(|sink| Box::new(SharedProgress(sink)) as Box<_>), + ) + .await + }); + }); + })); + Ok(Self { + cancellation, + subscription: Mutex::new(Some(subscription)), + }) + } + + #[allow(clippy::unnecessary_wraps)] // IDisposable is represented with the crate Result model. + pub(crate) fn native_dispose(&self) -> Result<(), Error> { + self.cancellation.cancel(); + self.subscription + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn progress_properties_round_trip_without_shared_mutability() { + let mut progress = InitialOutfitProgress::native_new().unwrap(); + progress.native_set_phase(Phase::Copying); + progress.native_set_total_items(7); + progress.native_set_items_copied(3); + progress.native_set_current_item_name("shirt".to_owned()); + progress.native_set_message("copying".to_owned()); + assert_eq!(progress.native_phase(), Phase::Copying); + assert_eq!(progress.native_total_items(), 7); + assert_eq!(progress.native_items_copied(), 3); + assert_eq!(progress.native_current_item_name(), "shirt"); + assert_eq!(progress.native_message(), "copying"); + } + + #[test] + fn find_node_by_name_walks_children_and_is_bounded() { + let mut root_data = InventoryFolder::new(UUID::random().unwrap()).unwrap(); + root_data.base.set_name("Library".to_owned()); + let root = InventoryNode::new_with_inventory_base(&root_data).unwrap(); + let mut child_data = InventoryFolder::new(UUID::random().unwrap()).unwrap(); + child_data.base.set_name("Starter Outfit".to_owned()); + let child = InventoryNode::new_with_inventory_base(&child_data).unwrap(); + root.nodes().add(child_data.base.uuid(), child).unwrap(); + let found = InitialOutfit::native_find_node_by_name(root, "Starter Outfit".to_owned()) + .unwrap() + .unwrap(); + assert_eq!( + found.data().unwrap().inventory_base().name(), + "Starter Outfit" + ); + } + + #[tokio::test] + async fn cancelled_create_does_not_touch_inventory_or_network() { + let client = GridClient::new().unwrap(); + let cof = CurrentOutfitFolder::native_new(Some(Arc::new(client.clone()))).unwrap(); + let initial = InitialOutfit::native_new(client, cof).unwrap(); + let source = CancellationTokenSource::new(); + source.cancel(); + let result = initial + .native_create_folder( + UUID::zero(), + "Clothing".to_owned(), + FolderType::Clothing, + Some(source.token()), + ) + .await; + assert_eq!(result, Err(Error::Cancelled)); + } + + #[test] + fn handler_dispose_is_idempotent_and_cancels_work() { + let handler = InitialOutfitHandler { + cancellation: CancellationTokenSource::new(), + subscription: Mutex::new(None), + }; + handler.native_dispose().unwrap(); + handler.native_dispose().unwrap(); + assert!(handler.cancellation.token().is_cancellation_requested()); + } +} diff --git a/crates/libremetaverse/src/lib.rs b/crates/libremetaverse/src/lib.rs index ab9fa6c..b6aa86a 100644 --- a/crates/libremetaverse/src/lib.rs +++ b/crates/libremetaverse/src/lib.rs @@ -7,11 +7,13 @@ mod attention_catalog; mod agent_manager; mod agent_messages; mod agent_movement; +mod appearance_baker; mod appearance_manager; mod asset_cache; mod asset_manager; mod asset_material; mod asset_models; +mod baking_texture_provider; mod bit_pack; mod caps; mod caps_http; @@ -20,6 +22,7 @@ mod current_outfit; mod download_manager; mod event_queue; mod gesture; +mod initial_outfit; mod inventory; mod inventory_ais; mod inventory_manager; @@ -166,13 +169,11 @@ impl NetworkManager { #[allow(dead_code)] impl imaging::Baker { fn resize_to_bake_dimensions( - _source: libremetaverse_imaging::ManagedImage, - _width: i32, - _height: i32, + source: libremetaverse_imaging::ManagedImage, + width: i32, + height: i32, ) -> Result, Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Baker.ResizeToBakeDimensions(LibreMetaverse.Imaging.ManagedImage,System.Int32,System.Int32)", - ) + crate::appearance_baker::Baker::native_resize_to_bake_dimensions(source, width, height) } } diff --git a/docs/appearance.md b/docs/appearance.md index 380afb9..6acb734 100644 --- a/docs/appearance.md +++ b/docs/appearance.md @@ -57,5 +57,72 @@ Focused compatibility coverage lives in `current_outfit_folder_semantics`, issue-relevant appearance cases. Native recording fakes additionally prove create/remove compensation, create-before-attach ordering, replacement state, layer preservation, and cancellation before backend interaction. Texture -baking, visual-parameter synthesis, and texture upload are a separate -appearance pipeline and are not owned by this service slice. +baking, visual-parameter synthesis, and texture upload are the next appearance +pipeline described below. + +## Baking and texture transport + +The baking pipeline retains all 45 avatar texture slots, all 11 classic and +extended baked outputs, the reference cache salts and sentinel UUIDs, and the +generated visual-parameter ordering from the pinned character definition. +Wearable assets are parsed as bounded LLWearable text, including parameter +values and texture indices. Texture retrieval is provided through +`IBakingTextureProvider`; the grid implementation downloads the asset and +decodes it through the existing JPEG2000 abstraction. At most five texture +downloads and six baked-texture uploads are in flight at once. + +`Baker` composites owned image copies into 1024-by-1024 bakes (128-by-128 for +eyes). It applies the canonical head, skin-grain, hair, wearable alpha, tint, +and morph-bump layers, preserves color/alpha/bump channels, and caps a bake at +60 input layers. Character resources are resolved cross-platform from +`LIBREMETAVERSE_RESOURCE_DIR`, the configured resource directory, or the +project resource bundle. Cached images are cloned on both insertion and read, +so a caller cannot corrupt a later bake. Minimal installations have neutral +built-in base masks; JPEG2000 encoding remains behind the project codec +feature rather than a platform API. + +Appearance setting first uses server-side baking when the simulator advertises +the appearance service and actually exposes `UpdateAvatarAppearance`; a region +with only the protocol flag falls back to local baking. The capability call +validates the COF version, success response, visual parameters, and required +baked texture IDs, with five cancellation-aware attempts and bounded +exponential delay. A classic region +requests cached bake hashes, incorporates the reply, downloads only necessary +inputs, bakes missing layers locally, uploads them, and sends the complete +`AgentSetAppearance` packet. Its texture entry uses the protocol's big-endian +seven-bit face masks and complete default-face attributes, while visual +parameters retain the reference's 218 normal or 251 physics-wearable fields +and drive the reported avatar height. Rebake packets cancel superseded work, +clear stale bake IDs, and force a fresh local bake. A region change before the +final send aborts and rolls back the old-region result. No synchronous state +lock crosses a network await or event callback. On cancellation or failure the +texture, wearable-asset, visual-parameter, and COF-version snapshots are +restored before the failure event is published. + +Wearable discovery rebuilds both wearables and attachments from resolved COF +links with a 512-entry bound. On classic/OpenSim regions, an empty or unusable +COF result falls back to `AgentWearablesRequest` and waits up to 30 seconds for +`AgentWearablesUpdate`. The packet handler ignores server-baking dummy payloads, +preserves every valid item/asset/type tuple, wakes the pending request, and can +start a bake for a spontaneous legacy update. Missing wearable assets or +required texture downloads fail the pipeline instead of publishing a false +success with incomplete bakes. + +## Initial outfit setup + +First-login outfit setup searches the library iteratively with a 256-node +bound, creates missing Clothing and Trash system folders, counts the selected +tree, and copies its folder and item hierarchy into Clothing. Progress follows +`Counting`, `Copying`, `Applying`, then `Complete`; completion is reported only +after transactional COF replacement succeeds. Cancellation is checked during +planning, creation, copying, and application. A failed or cancelled copy +removes its partially created root, and a failed application removes the fully +copied root while the Current Outfit Folder restores its prior links. The +login handler starts this work once, owns its cancellation source and event +subscription, and disposes idempotently. + +Focused verification covers the translated Baker cases, exact bake mappings +and cache hashes, generated visual metadata, wearable parsing, output pixels +and channels, packet/event payloads, cancellation before mutation, progress +state, and handler disposal. Live appearance tests remain opt-in because they +require grid credentials. diff --git a/tests/PARITY.md b/tests/PARITY.md index f013785..129dce0 100644 --- a/tests/PARITY.md +++ b/tests/PARITY.md @@ -121,10 +121,10 @@ Reviewed Rust tests live outside `generated_parity.rs` and carry a `parity-case` | `LibreMetaverse.Tests/AvatarManagerTests.cs::AvatarManagerTests.UpdateLastReceivedCOFVersion_RejectsLowerVersion::test` | `AvatarManagerTests.UpdateLastReceivedCOFVersion_RejectsLowerVersion` | `` | `LibreMetaverse.Tests/AvatarManagerTests.cs:68` | `Appearance` | `` | `crates/libremetaverse/src/avatar_animesh_semantics.rs:612 (`appearance_manager_update_last_received_cof_version_rejects_lower_version`)` | `translated` | `f4a827fcace9d50306a8c375183e056140996d2a39b02e4dd12fc6cc79e5d0f0` | | `LibreMetaverse.Tests/AvatarManagerTests.cs::AvatarManagerTests.UpdateLastReceivedCOFVersion_EqualVersion_IsNotAdvanced::test` | `AvatarManagerTests.UpdateLastReceivedCOFVersion_EqualVersion_IsNotAdvanced` | `` | `LibreMetaverse.Tests/AvatarManagerTests.cs:81` | `Appearance` | `` | `crates/libremetaverse/src/avatar_animesh_semantics.rs:627 (`appearance_manager_update_last_received_cof_version_equal_version_is_not_advanced`)` | `translated` | `e3db5962cdad5e18ad27a36d0d7cca2d471e96df7513315d0f34bb0db99d723c` | | `LibreMetaverse.Tests/BakerTests.cs::BakerTests.Bake_GrayscaleLayerTexture_DoesNotThrow::test` | `BakerTests.Bake_GrayscaleLayerTexture_DoesNotThrow` | `` | `LibreMetaverse.Tests/BakerTests.cs:15` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:30 (`bake_grayscale_layer_texture_does_not_throw`)` | `translated` | `c3fc1d2c16b5f8d69678a6cf05a3a275e0b657f3a6e7f6788f350f2a107200b3` | -| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.Bake_GrayscaleHairTextureNoSkin_DoesNotThrow::test` | `BakerTests.Bake_GrayscaleHairTextureNoSkin_DoesNotThrow` | `` | `LibreMetaverse.Tests/BakerTests.cs:37` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:41 (`bake_grayscale_hair_texture_no_skin_does_not_throw`)` | `translated` | `e48b737d9138e63cda98b5e80bf561a80d1dab5de78901ac9e1bb320ca281eb6` | -| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.LoadResourceLayer_HeadColorTga_LoadsFromCharacterDirectory::test` | `BakerTests.LoadResourceLayer_HeadColorTga_LoadsFromCharacterDirectory` | `` | `LibreMetaverse.Tests/BakerTests.cs:62` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:55 (`load_resource_layer_head_color_tga_loads_from_character_directory`)` | `translated` | `5a9b80005a51f04a227ca392218725d50704d038783e04fc49bb955997c22e76` | -| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.LoadResourceLayer_CachedResourceNotCorruptedByCallerMutation::test` | `BakerTests.LoadResourceLayer_CachedResourceNotCorruptedByCallerMutation` | `` | `LibreMetaverse.Tests/BakerTests.cs:69` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:65 (`load_resource_layer_cached_resource_not_corrupted_by_caller_mutation`)` | `translated` | `18b458bbd7bb13d2b68711f7d3495193f1b4edac9c1f776930ad2c172f97cd49` | -| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.ResizeToBakeDimensions_SourceSmallerInOneAxisOnly_ScalesRatherThanTiles::test` | `BakerTests.ResizeToBakeDimensions_SourceSmallerInOneAxisOnly_ScalesRatherThanTiles` | `` | `LibreMetaverse.Tests/BakerTests.cs:93` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:84 (`resize_to_bake_dimensions_source_smaller_in_one_axis_only_scales_rather_than_tiles`)` | `translated` | `ce5bd346b48cb19dbc0248403eafefb07309d4e05e675e9a02db5a46d7cd65ff` | +| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.Bake_GrayscaleHairTextureNoSkin_DoesNotThrow::test` | `BakerTests.Bake_GrayscaleHairTextureNoSkin_DoesNotThrow` | `` | `LibreMetaverse.Tests/BakerTests.cs:37` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:46 (`bake_grayscale_hair_texture_no_skin_does_not_throw`)` | `translated` | `e48b737d9138e63cda98b5e80bf561a80d1dab5de78901ac9e1bb320ca281eb6` | +| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.LoadResourceLayer_HeadColorTga_LoadsFromCharacterDirectory::test` | `BakerTests.LoadResourceLayer_HeadColorTga_LoadsFromCharacterDirectory` | `` | `LibreMetaverse.Tests/BakerTests.cs:62` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:60 (`load_resource_layer_head_color_tga_loads_from_character_directory`)` | `translated` | `5a9b80005a51f04a227ca392218725d50704d038783e04fc49bb955997c22e76` | +| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.LoadResourceLayer_CachedResourceNotCorruptedByCallerMutation::test` | `BakerTests.LoadResourceLayer_CachedResourceNotCorruptedByCallerMutation` | `` | `LibreMetaverse.Tests/BakerTests.cs:69` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:70 (`load_resource_layer_cached_resource_not_corrupted_by_caller_mutation`)` | `translated` | `18b458bbd7bb13d2b68711f7d3495193f1b4edac9c1f776930ad2c172f97cd49` | +| `LibreMetaverse.Tests/BakerTests.cs::BakerTests.ResizeToBakeDimensions_SourceSmallerInOneAxisOnly_ScalesRatherThanTiles::test` | `BakerTests.ResizeToBakeDimensions_SourceSmallerInOneAxisOnly_ScalesRatherThanTiles` | `` | `LibreMetaverse.Tests/BakerTests.cs:93` | `Imaging` | `` | `crates/libremetaverse/src/appearance_baker_semantics.rs:89 (`resize_to_bake_dimensions_source_smaller_in_one_axis_only_scales_rather_than_tiles`)` | `translated` | `ce5bd346b48cb19dbc0248403eafefb07309d4e05e675e9a02db5a46d7cd65ff` | | `LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_ClothingWearable_ReturnsWearableTypeAndZeroLayer::test` | `BatchLinkTests.GetCofLinkDescription_ClothingWearable_ReturnsWearableTypeAndZeroLayer` | `` | `LibreMetaverse.Tests/BatchLinkTests.cs:84` | `` | `LibreMetaverse.Tests/TestHelpers/FakeGridClient.cs` | `crates/libremetaverse/src/batch_link_internal_semantics.rs:151 (`get_cof_link_description_clothing_wearable_returns_wearable_type_and_zero_layer`)` | `translated` | `898a14ff28d953a58a01c3a0e5d9d9fcbbe61376a33fdc5caf20268aaac76242` | | `LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_BodyPartShape_ReturnsEmpty::test` | `BatchLinkTests.GetCofLinkDescription_BodyPartShape_ReturnsEmpty` | `` | `LibreMetaverse.Tests/BatchLinkTests.cs:98` | `` | `LibreMetaverse.Tests/TestHelpers/FakeGridClient.cs` | `crates/libremetaverse/src/batch_link_internal_semantics.rs:160 (`get_cof_link_description_body_part_shape_returns_empty`)` | `translated` | `4f009136d265da91afb9eafd060faf459f6c510ca0502a33ce9e128d241f9bd1` | | `LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_AttachmentObject_ReturnsEmpty::test` | `BatchLinkTests.GetCofLinkDescription_AttachmentObject_ReturnsEmpty` | `` | `LibreMetaverse.Tests/BatchLinkTests.cs:112` | `` | `LibreMetaverse.Tests/TestHelpers/FakeGridClient.cs` | `crates/libremetaverse/src/batch_link_internal_semantics.rs:169 (`get_cof_link_description_attachment_object_returns_empty`)` | `translated` | `a838329a7a23081a4022671bc794e7e706c9ab7da51598280a8befbb824d336d` | diff --git a/tests/upstream-tests.json b/tests/upstream-tests.json index d69c3f5..08b5a8e 100644 --- a/tests/upstream-tests.json +++ b/tests/upstream-tests.json @@ -2144,7 +2144,7 @@ "Imaging" ], "fixture_dependencies": [], - "rust_body_sha256": "095b472ba775ae13542328da1b5e709d630699d092778132b3c1f164168c5dd8", + "rust_body_sha256": "5c686ee0bd308a54e16a0ea8b50e1e75f9b085dc0cb9bb45317bde0c5cac674a", "status": "translated", "rust_file": "crates/libremetaverse/src/appearance_baker_semantics.rs", "rust_line": 30, @@ -2168,7 +2168,7 @@ "rust_body_sha256": "e490a240143cf358cedc6562baa63dd9ee2274958744e7df00d13d9981f7597f", "status": "translated", "rust_file": "crates/libremetaverse/src/appearance_baker_semantics.rs", - "rust_line": 41, + "rust_line": 46, "rust_test": "bake_grayscale_hair_texture_no_skin_does_not_throw", "semantic_review": "reviewed" }, @@ -2189,7 +2189,7 @@ "rust_body_sha256": "8c7043c839d0a52cc72050120aaf3f726b962ee674e9169c33d8b1137b23d919", "status": "translated", "rust_file": "crates/libremetaverse/src/appearance_baker_semantics.rs", - "rust_line": 55, + "rust_line": 60, "rust_test": "load_resource_layer_head_color_tga_loads_from_character_directory", "semantic_review": "reviewed" }, @@ -2210,7 +2210,7 @@ "rust_body_sha256": "54a79b572fdf3e22a3a00380cb98f180a2eda94f71ed035db312310b8f15a9bb", "status": "translated", "rust_file": "crates/libremetaverse/src/appearance_baker_semantics.rs", - "rust_line": 65, + "rust_line": 70, "rust_test": "load_resource_layer_cached_resource_not_corrupted_by_caller_mutation", "semantic_review": "reviewed" }, @@ -2231,7 +2231,7 @@ "rust_body_sha256": "16a16384fe8cfa5c8a8a4b8de9bf9428de61756882d83883a7215ef708fa202d", "status": "translated", "rust_file": "crates/libremetaverse/src/appearance_baker_semantics.rs", - "rust_line": 84, + "rust_line": 89, "rust_test": "resize_to_bake_dimensions_source_smaller_in_one_axis_only_scales_rather_than_tiles", "semantic_review": "reviewed" }, @@ -9003,7 +9003,7 @@ "body_sha256": "0e0f06d2317de91147bab1e623bed35ee03015da1498a9b9837c91c144bf7da5", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "7ec1c1c19905ad5733028faae50f2f20f81216faecf6ab13d60c54f4e7cdc427", + "rust_body_sha256": "333271eabd854cb7150441d999d3f6cb1c2fcf00234ce5e490bf54828f620b91", "status": "translated", "rust_file": "tests/compat/tests/wire_semantics.rs", "rust_line": 329, diff --git a/tools/check_milestone_09_issue_66.py b/tools/check_milestone_09_issue_66.py new file mode 100644 index 0000000..0bed906 --- /dev/null +++ b/tools/check_milestone_09_issue_66.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Audit issue 66's appearance baking and initial-outfit boundary.""" + +from __future__ import annotations + +from pathlib import Path +import re + +import generate_api_shims + + +ROOT = Path(__file__).resolve().parents[1] +APPEARANCE = ROOT / "crates" / "libremetaverse" / "src" / "appearance_manager.rs" +BAKER = ROOT / "crates" / "libremetaverse" / "src" / "appearance_baker.rs" +PROVIDER = ROOT / "crates" / "libremetaverse" / "src" / "baking_texture_provider.rs" +OUTFIT = ROOT / "crates" / "libremetaverse" / "src" / "initial_outfit.rs" +ASSETS = ROOT / "crates" / "libremetaverse" / "src" / "asset_models.rs" +BAKER_TESTS = ROOT / "crates" / "libremetaverse" / "src" / "appearance_baker_semantics.rs" +VISUAL_TESTS = ROOT / "crates" / "libremetaverse" / "tests" / "generated_catalogs.rs" +COMPAT = ROOT / "tests" / "compat" / "tests" / "appearance_semantics.rs" +DOC = ROOT / "docs" / "appearance.md" +STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") + +REQUIRED_TYPES = { + "T:LibreMetaverse.AppearanceManager", + "T:LibreMetaverse.Appearance.InitialOutfit", + "T:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress", + "T:LibreMetaverse.Appearance.InitialOutfitHandler", + "T:LibreMetaverse.GridClientBakingTextureProvider", + "T:LibreMetaverse.Imaging.Baker", +} + +REQUIRED_MEMBERS = { + "M:LibreMetaverse.AppearanceManager.DecodeWearableParams(LibreMetaverse.AppearanceManager.WearableData,LibreMetaverse.AppearanceManager.TextureData[]@)", + "M:LibreMetaverse.AppearanceManager.MakeAppearancePacket", + "M:LibreMetaverse.AppearanceManager.RequestCachedBakes", + "M:LibreMetaverse.AppearanceManager.RequestSetAppearance(System.Boolean)", + "M:LibreMetaverse.AppearanceManager.ServerBakingRegion", + "M:LibreMetaverse.GridClientBakingTextureProvider.RequestTextureAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)", + "M:LibreMetaverse.Imaging.Baker.Bake", + "M:LibreMetaverse.Imaging.Baker.LoadResourceLayer(System.String)", + "M:LibreMetaverse.Appearance.InitialOutfit.CopyFolderAsync(LibreMetaverse.InventoryFolder,LibreMetaverse.UUID,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})", + "M:LibreMetaverse.Appearance.InitialOutfit.SetInitialOutfitAsync(System.String,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})", +} + + +def require_markers(path: Path, markers: tuple[str, ...]) -> None: + text = path.read_text() + missing = [marker for marker in markers if marker not in text] + if missing: + raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing)) + + +def main() -> None: + native = set(generate_api_shims.NATIVE_TYPES) | set(generate_api_shims.NATIVE_DECLARATIONS) + missing_types = sorted(REQUIRED_TYPES - native) + if missing_types: + raise SystemExit("issue 66 native types missing: " + ", ".join(missing_types)) + missing_members = sorted(REQUIRED_MEMBERS - set(generate_api_shims.NATIVE_MEMBER_BODIES)) + if missing_members: + raise SystemExit("issue 66 native members missing: " + ", ".join(missing_members)) + for source in (APPEARANCE, BAKER, PROVIDER, OUTFIT, ASSETS): + if STUB_RE.search(source.read_text()): + raise SystemExit(f"issue 66 owned Rust stubs remain in {source.name}") + + require_markers(APPEARANCE, ( + "baked_texture_hash", "wearable_bake_map", "AgentCachedTextureResponse", + "AgentWearablesRequest", "AgentWearablesUpdate", "MAX_COF_ENTRIES", + "RebakeAvatarTextures", "tokio::sync::Semaphore::new(5)", + "tokio::sync::Semaphore::new(6)", "native_server_bake", + "old_visual_parameters", "appearance_packet_contains_all_bakes", + "legacy_wearables_packet_preserves_valid_item_asset_and_type_fields", + )) + require_markers(BAKER, ( + "CLASSIC_BAKE_SIZE", "EYE_BAKE_SIZE", "self.textures.len() >= 60", "head_color.tga", + "head_skingrain.tga", "apply_parameter_mask", "output.bump", + "native_deep_clone", '#[cfg(feature = "jpeg2000")]', + )) + require_markers(PROVIDER, ("ClientWeakHandle", "request_image", "native_decode_image")) + require_markers(OUTFIT, ( + "MAX_FOLDERS", "Phase::Counting", "Phase::Copying", "Phase::Applying", + "cleanup_folder", "compare_exchange", "native_replace_outfit", + "handler_dispose_is_idempotent", + )) + require_markers(ASSETS, ( + "native_from_asset", "parameters ", "textures ", + "wearable_parser_preserves_parameters_and_texture_indices", + )) + require_markers(BAKER_TESTS, ("parity-case", "image.red[0]", "image.bump[0]")) + require_markers(VISUAL_TESTS, ("visual_catalog_matches_the_pinned_character_fixture", "672", "253")) + require_markers(COMPAT, ("baked_texture_hash", "wearable_bake_map", "texture_provider")) + require_markers(DOC, ( + "all 11", "downloads and six baked-texture uploads", + "Counting`, `Copying`, `Applying`, then `Complete", "256-node", + "big-endian", "218 normal or 251", "UpdateAvatarAppearance", + )) + print( + "issue 66 audit: native bake composition, bounded texture transport, cache/server/local " + "appearance paths, transactional initial outfit, documentation, and focused evidence are present" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index a342fa4..3a61e48 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -292,13 +292,20 @@ NATIVE_TYPES = { NATIVE_DECLARATIONS = { "T:LibreMetaverse.AgentManager": "crate::agent_manager::AgentManager", "T:LibreMetaverse.AppearanceManager": "crate::appearance_manager::AppearanceManager", + "T:LibreMetaverse.AppearanceSetEventArgs": "crate::appearance_manager::AppearanceSetEventArgs", "T:LibreMetaverse.Appearance.CompositeCurrentOutfitPolicy": "crate::current_outfit::CompositeCurrentOutfitPolicy", "T:LibreMetaverse.Appearance.CurrentOutfitFolder": "crate::current_outfit::CurrentOutfitFolder", + "T:LibreMetaverse.Appearance.InitialOutfit": "crate::initial_outfit::InitialOutfit", + "T:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress": "crate::initial_outfit::InitialOutfitProgress", + "T:LibreMetaverse.Appearance.InitialOutfitHandler": "crate::initial_outfit::InitialOutfitHandler", + "T:LibreMetaverse.GridClientBakingTextureProvider": "crate::baking_texture_provider::GridClientBakingTextureProvider", + "T:LibreMetaverse.Imaging.Baker": "crate::appearance_baker::Baker", "T:LibreMetaverse.GridClient": "crate::client_core::GridClient", "T:LibreMetaverse.InventoryManager": "crate::inventory_manager::InventoryManager", "T:LibreMetaverse.InventoryAISClient": "crate::inventory_ais::InventoryAISClient", "T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager", "T:LibreMetaverse.ObjectManager": "crate::object_material::ObjectManager", + "T:LibreMetaverse.RebakeAvatarTexturesEventArgs": "crate::appearance_manager::RebakeAvatarTexturesEventArgs", "T:LibreMetaverse.Settings": "crate::client_core::Settings", "T:LibreMetaverse.Simulator": "crate::network_manager::Simulator", "T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser", @@ -371,18 +378,106 @@ NATIVE_MEMBER_BODIES = { "self.native_set_cof(value)", "P:LibreMetaverse.Appearance.CurrentOutfitFolder.MaxClothingLayers": "crate::current_outfit::MAX_CLOTHING_LAYERS", + "M:LibreMetaverse.Appearance.InitialOutfit.#ctor(LibreMetaverse.GridClient,LibreMetaverse.Appearance.CurrentOutfitFolder)": + "crate::initial_outfit::InitialOutfit::native_new(client, cof)", + "M:LibreMetaverse.Appearance.InitialOutfit.CheckSystemFoldersAsync(System.Threading.CancellationToken)": + "self.native_check_system_folders(cancellation_token).await", + "M:LibreMetaverse.Appearance.InitialOutfit.CopyFolderAsync(LibreMetaverse.InventoryFolder,LibreMetaverse.UUID,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})": + "self.native_copy_folder(folder, destination, cancellation_token, progress).await", + "M:LibreMetaverse.Appearance.InitialOutfit.CreateFolderAsync(LibreMetaverse.UUID,System.String,LibreMetaverse.FolderType,System.Threading.CancellationToken)": + "self.native_create_folder(parent, name, type_, cancellation_token).await", + "M:LibreMetaverse.Appearance.InitialOutfit.FindNodeByName(LibreMetaverse.InventoryNode,System.String)": + "crate::initial_outfit::InitialOutfit::native_find_node_by_name(root, name)", + "M:LibreMetaverse.Appearance.InitialOutfit.SetInitialOutfitAsync(System.String,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})": + "self.native_set_initial_outfit(outfit, cancellation_token, progress).await", + "M:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.#ctor": + "crate::initial_outfit::InitialOutfitProgress::native_new()", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.CurrentItemName": + "self.native_current_item_name()", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.CurrentItemName#set": + "self.native_set_current_item_name(value)", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.ItemsCopied": + "self.native_items_copied()", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.ItemsCopied#set": + "self.native_set_items_copied(value)", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Message": + "self.native_message()", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Message#set": + "self.native_set_message(value)", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Phase": + "self.native_phase()", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.Phase#set": + "self.native_set_phase(value)", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.TotalItems": + "self.native_total_items()", + "P:LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress.TotalItems#set": + "self.native_set_total_items(value)", + "M:LibreMetaverse.Appearance.InitialOutfitHandler.#ctor(LibreMetaverse.GridClient,LibreMetaverse.Appearance.CurrentOutfitFolder,System.IProgress{LibreMetaverse.Appearance.InitialOutfit.InitialOutfitProgress})": + "crate::initial_outfit::InitialOutfitHandler::native_new(client, cof, progress)", + "M:LibreMetaverse.Appearance.InitialOutfitHandler.Dispose": + "self.native_dispose()", "M:LibreMetaverse.AppearanceManager.#ctor(LibreMetaverse.GridClient)": "crate::appearance_manager::AppearanceManager::native_new(client)", + "M:LibreMetaverse.AppearanceSetEventArgs.#ctor(System.Boolean)": + "Ok(crate::appearance_manager::AppearanceSetEventArgs::native_new(success))", + "P:LibreMetaverse.AppearanceSetEventArgs.Success": + "self.native_success()", + "M:LibreMetaverse.RebakeAvatarTexturesEventArgs.#ctor(LibreMetaverse.UUID)": + "Ok(crate::appearance_manager::RebakeAvatarTexturesEventArgs::native_new(texture_id))", + "P:LibreMetaverse.RebakeAvatarTexturesEventArgs.TextureID": + "self.native_texture_id()", "E:LibreMetaverse.AppearanceManager.AgentWearablesReply": "self.native_subscribe_agent_wearables_reply(handler)", "E:LibreMetaverse.AppearanceManager.AppearanceSet": "self.native_subscribe_appearance_set(handler)", + "E:LibreMetaverse.AppearanceManager.CachedBakesReply": + "self.native_subscribe_cached_bakes_reply(handler)", + "E:LibreMetaverse.AppearanceManager.RebakeAvatarRequested": + "self.native_subscribe_rebake_avatar_requested(handler)", + "F:LibreMetaverse.AppearanceManager.ATTACHMENT_ADD": + "crate::appearance_manager::ATTACHMENT_ADD", + "F:LibreMetaverse.AppearanceManager.BAKED_TEXTURE_HASH": + "crate::appearance_manager::baked_texture_hash()", + "F:LibreMetaverse.AppearanceManager.BakeIndexToTextureIndex": + "crate::appearance_manager::bake_index_to_texture_index()", + "F:LibreMetaverse.AppearanceManager.DEFAULT_AVATAR_TEXTURE": + "crate::appearance_manager::default_avatar_texture()", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX1": + "crate::appearance_manager::img_use_baked(crate::BakeType::BakedAux1)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX2": + "crate::appearance_manager::img_use_baked(crate::BakeType::BakedAux2)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_AUX3": + "crate::appearance_manager::img_use_baked(crate::BakeType::BakedAux3)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_EYES": + "crate::appearance_manager::img_use_baked(crate::BakeType::Eyes)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_HAIR": + "crate::appearance_manager::img_use_baked(crate::BakeType::Hair)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_HEAD": + "crate::appearance_manager::img_use_baked(crate::BakeType::Head)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_INDICES": + "crate::appearance_manager::img_use_baked_indices()", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LEFTARM": + "crate::appearance_manager::img_use_baked(crate::BakeType::BakedLeftArm)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LEFTLEG": + "crate::appearance_manager::img_use_baked(crate::BakeType::BakedLeftLeg)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_LOWER": + "crate::appearance_manager::img_use_baked(crate::BakeType::LowerBody)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_SKIRT": + "crate::appearance_manager::img_use_baked(crate::BakeType::Skirt)", + "F:LibreMetaverse.AppearanceManager.IMG_USE_BAKED_UPPER": + "crate::appearance_manager::img_use_baked(crate::BakeType::UpperBody)", + "F:LibreMetaverse.AppearanceManager.WEARABLE_BAKE_MAP": + "crate::appearance_manager::wearable_bake_map()", "M:LibreMetaverse.AppearanceManager.AddAttachments(System.Collections.Generic.List{LibreMetaverse.InventoryItem},System.Boolean,System.Boolean)": "self.native_add_attachments(attachments, remove_existing_first, replace.unwrap_or(false))", "M:LibreMetaverse.AppearanceManager.AddToOutfit(LibreMetaverse.InventoryItem,System.Boolean)": "self.native_add_to_outfit(vec![wearable_item], replace.unwrap_or(false))", "M:LibreMetaverse.AppearanceManager.AddToOutfit(System.Collections.Generic.List{LibreMetaverse.InventoryItem},System.Boolean)": "self.native_add_to_outfit(wearable_items, replace.unwrap_or(false))", + "M:LibreMetaverse.AppearanceManager.BakeTypeToAgentTextureIndex(LibreMetaverse.BakeType)": + "Ok(crate::appearance_manager::bake_type_to_agent_texture_index(index))", + "M:LibreMetaverse.AppearanceManager.BakeTypeToTextures(LibreMetaverse.BakeType)": + "Ok(crate::appearance_manager::bake_type_to_textures(bake_type))", "M:LibreMetaverse.AppearanceManager.Attach(LibreMetaverse.InventoryItem,LibreMetaverse.AttachmentPoint,System.Boolean)": "self.native_attach(item, attach_point, replace.unwrap_or(false))", "M:LibreMetaverse.AppearanceManager.Attach(LibreMetaverse.UUID,LibreMetaverse.UUID,System.String,System.String,LibreMetaverse.Permissions,System.UInt32,LibreMetaverse.AttachmentPoint,System.Boolean)": @@ -391,6 +486,8 @@ NATIVE_MEMBER_BODIES = { "self.native_detach(item.base.uuid())", "M:LibreMetaverse.AppearanceManager.Detach(LibreMetaverse.UUID)": "self.native_detach(item_id)", + "M:LibreMetaverse.AppearanceManager.DecodeWearableParams(LibreMetaverse.AppearanceManager.WearableData,LibreMetaverse.AppearanceManager.TextureData[]@)": + "crate::appearance_manager::decode_wearable_params(wearable, textures)", "M:LibreMetaverse.AppearanceManager.Dispose": "self.native_dispose()", "M:LibreMetaverse.AppearanceManager.GetAttachments": @@ -403,6 +500,8 @@ NATIVE_MEMBER_BODIES = { "self.native_get_attachments_by_id()", "M:LibreMetaverse.AppearanceManager.GetCurrentOutfitFolderAsync(System.Threading.CancellationToken)": "self.native_get_current_outfit_folder(cancellation_token).await", + "M:LibreMetaverse.AppearanceManager.GetColorFromParams(System.Collections.Generic.List{LibreMetaverse.AppearanceManager.ColorParamInfo})": + "crate::appearance_manager::color_from_params(param)", "M:LibreMetaverse.AppearanceManager.GetFolderWearablesAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)": "self.native_get_folder_wearables(folder, cancellation_token).await", "M:LibreMetaverse.AppearanceManager.GetWearableAssets(LibreMetaverse.WearableType)": @@ -411,6 +510,8 @@ NATIVE_MEMBER_BODIES = { "self.native_get_wearables()", "M:LibreMetaverse.AppearanceManager.GetWearablesByType": "self.native_get_wearables_by_type()", + "M:LibreMetaverse.AppearanceManager.GetCurrentParamValues": + "self.native_get_current_param_values()", "M:LibreMetaverse.AppearanceManager.IsItemWorn(LibreMetaverse.InventoryItem)": "self.native_is_item_worn(item.base.uuid())", "M:LibreMetaverse.AppearanceManager.IsItemWorn(LibreMetaverse.UUID)": @@ -425,6 +526,16 @@ NATIVE_MEMBER_BODIES = { "self.native_replace_outfit(wearable_items, safe).await", "M:LibreMetaverse.AppearanceManager.RequestAgentWornAsync(System.Threading.CancellationToken)": "self.native_request_agent_worn(cancellation_token).await", + "M:LibreMetaverse.AppearanceManager.MakeAppearancePacket": + "self.native_make_appearance_packet()", + "M:LibreMetaverse.AppearanceManager.MorphLayerForBakeType(LibreMetaverse.BakeType)": + "Ok(crate::appearance_manager::morph_layer_for_bake_type(bake_type))", + "M:LibreMetaverse.AppearanceManager.RequestCachedBakes": + "self.native_request_cached_bakes()", + "M:LibreMetaverse.AppearanceManager.RequestSetAppearance(System.Boolean)": + "self.native_request_set_appearance(force_rebake.unwrap_or(false)).await", + "M:LibreMetaverse.AppearanceManager.ServerBakingRegion": + "self.native_server_baking_region()", "M:LibreMetaverse.AppearanceManager.SendOutfitToCurrentSimulatorAsync(System.Threading.CancellationToken)": "self.native_send_outfit(cancellation_token).await", "M:LibreMetaverse.AppearanceManager.UpdateLastReceivedCOFVersion(System.Int32)": @@ -441,6 +552,42 @@ NATIVE_MEMBER_BODIES = { "self.native_last_cof_version()", "P:LibreMetaverse.AppearanceManager.ManagerBusy": "self.native_manager_busy()", + "P:LibreMetaverse.AppearanceManager.TextureProvider": + "self.native_texture_provider()", + "P:LibreMetaverse.AppearanceManager.TextureProvider#set": + "self.native_set_texture_provider(value)", + "M:LibreMetaverse.AppearanceManager.TextureData.#ctor": + "crate::appearance_manager::texture_data_defaults()", + "M:LibreMetaverse.AppearanceManager.TextureData.ToString": + "crate::appearance_manager::texture_data_to_string(self)", + "M:LibreMetaverse.AppearanceManager.WearableData.#ctor": + "crate::appearance_manager::wearable_data_defaults()", + "M:LibreMetaverse.AppearanceManager.WearableData.ToString": + "crate::appearance_manager::wearable_data_to_string(self)", + "M:LibreMetaverse.GridClientBakingTextureProvider.#ctor(LibreMetaverse.GridClient)": + "crate::baking_texture_provider::GridClientBakingTextureProvider::native_new(client)", + "M:LibreMetaverse.GridClientBakingTextureProvider.RequestTextureAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)": + "self.native_request_texture(texture_id, cancellation_token).await", + "M:LibreMetaverse.Imaging.Baker.#ctor(LibreMetaverse.BakeType)": + "crate::appearance_baker::Baker::native_new(bake_type)", + "M:LibreMetaverse.Imaging.Baker.AddTexture(LibreMetaverse.AppearanceManager.TextureData)": + "self.native_add_texture(tdata)", + "M:LibreMetaverse.Imaging.Baker.Bake": + "self.native_bake()", + "M:LibreMetaverse.Imaging.Baker.BakeTypeFor(LibreMetaverse.AvatarTextureIndex)": + "Ok(crate::appearance_baker::Baker::native_bake_type_for(index))", + "M:LibreMetaverse.Imaging.Baker.LoadResourceLayer(System.String)": + "crate::appearance_baker::Baker::native_load_resource_layer(file_name)", + "P:LibreMetaverse.Imaging.Baker.BakeHeight": + "self.native_bake_height()", + "P:LibreMetaverse.Imaging.Baker.BakeType": + "self.native_bake_type()", + "P:LibreMetaverse.Imaging.Baker.BakeWidth": + "self.native_bake_width()", + "P:LibreMetaverse.Imaging.Baker.BakedTexture": + "self.native_baked_texture()", + "P:LibreMetaverse.Imaging.Baker.Textures": + "self.native_textures()", "P:LibreMetaverse.GridClient.Appearance": "self.native_appearance().unwrap_or_else(|_| panic!(\"failed to construct AppearanceManager\"))", "P:LibreMetaverse.GridClient.Appearance#set": @@ -1787,7 +1934,7 @@ NATIVE_MESSAGE_CODEC_TYPES = { } OPEN_ENUM_IDS = {"T:LibreMetaverse.BakeType", "T:LibreMetaverse.HoleType"} TRAIT_SUPERTRAITS = { - "T:LibreMetaverse.IBakingTextureProvider": "std::any::Any", + "T:LibreMetaverse.IBakingTextureProvider": "std::any::Any + Send + Sync", "T:LibreMetaverse.Interfaces.IMessage": "std::any::Any", "T:LibreMetaverse.Appearance.ICurrentOutfitPolicy": "Send + Sync", } diff --git a/tools/generate_rust_mapping.py b/tools/generate_rust_mapping.py index 95a445f..53ae8aa 100644 --- a/tools/generate_rust_mapping.py +++ b/tools/generate_rust_mapping.py @@ -989,7 +989,9 @@ def validate_generated_shims() -> None: "crate::asset_manager::", "crate::asset_material::", "crate::asset_models::", + "crate::appearance_baker::", "crate::appearance_manager::", + "crate::baking_texture_provider::", "crate::byte_order::", "crate::attention_catalog::", "crate::binary::", @@ -1001,6 +1003,7 @@ def validate_generated_shims() -> None: "crate::inventory::", "crate::inventory_ais::", "crate::inventory_manager::", + "crate::initial_outfit::", "crate::current_outfit::", "crate::message_codec::", "crate::message_decoder::",