Implement appearance baking pipeline (#66)
Some checks failed
Native code generation / deterministic (push) Failing after 2m18s
Imaging and meshing gate / native (push) Failing after 1m28s
Native Rust workspace compile / compile (push) Failing after 58s

This commit is contained in:
2026-08-10 10:33:29 +00:00
parent a9927fcfcb
commit b1f60db3d6
17 changed files with 3487 additions and 249 deletions

View File

@@ -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<HashMap<String, ManagedImage>> {
static CACHE: OnceLock<Mutex<HashMap<String, ManagedImage>>> = 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<AppearanceManagerTextureData>,
baked_texture: Option<crate::assets::AssetTexture>,
}
impl Baker {
#[allow(clippy::unnecessary_wraps)] // The generated constructor uses the crate Result model.
pub(crate) fn native_new(bake_type: BakeType) -> Result<Self, Error> {
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<Option<ManagedImage>, 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<Option<ManagedImage>, 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<crate::assets::AssetTexture> {
self.baked_texture
.as_ref()
.and_then(|texture| texture.native_deep_clone().ok())
}
pub(crate) fn native_textures(&self) -> Vec<AppearanceManagerTextureData> {
self.textures
.iter()
.filter_map(clone_texture_data)
.collect()
}
}
fn clone_texture_data(
layer: &AppearanceManagerTextureData,
) -> Option<AppearanceManagerTextureData> {
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<PathBuf> {
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<Option<ManagedImage>, 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<ManagedImage, Error> {
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, &param.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, &param.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<u8> {
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);
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -464,6 +464,52 @@ pub struct AssetTexture {
pub image: Option<ManagedImage>,
}
impl AssetTexture {
pub(crate) fn native_bytes(&self) -> Vec<u8> {
self.raw.bytes()
}
pub(crate) fn native_deep_clone(&self) -> Result<Self, Error> {
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<bool, Error> {
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<Self, Error> {
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<Self, Error> {
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::<usize>().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::<i32>(), value.parse::<f32>())
{
result.params.insert(id, value);
}
}
index = index.saturating_add(count);
} else if let Some(count) = line
.strip_prefix("textures ")
.and_then(|value| value.parse::<usize>().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::<i32>(), 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<bool, Error> {
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)
));
}
}

View File

@@ -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<Self, Error> {
Ok(Self {
client: client.native_weak_handle(),
})
}
pub(crate) async fn native_request_texture(
&self,
texture_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<crate::assets::AssetTexture>, 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<CancellationToken>,
) -> Pin<Box<dyn Future<Output = Result<Option<crate::assets::AssetTexture>, Error>> + Send + '_>>
{
Box::pin(self.native_request_texture(texture_id, cancellation_token))
}
}

View File

@@ -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::UUID> {
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<u8> {
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<Vec<libremetaverse_types::WearableType>> {
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::AvatarTextureIndex, crate::Error> {
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<Vec<libremetaverse::AvatarTextureIndex>, 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<libremetaverse::AppearanceManagerTextureData>,
) -> 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<libremetaverse::AppearanceManagerColorParamInfo>,
) -> Result<libremetaverse_types::Color4, crate::Error> {
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<std::collections::HashMap<i32, f32>, 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::packets::AgentSetAppearancePacket, crate::Error> {
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::AvatarTextureIndex, crate::Error> {
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<bool>,
) -> 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<bool, crate::Error> {
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<dyn libremetaverse::IBakingTextureProvider> {
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<dyn libremetaverse::IBakingTextureProvider>,
) {
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<Self, crate::Error> {
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<Self, crate::Error> {
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<Self, crate::Error> {
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<Self, crate::Error> {
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<libremetaverse_types::compat::CancellationToken>,
) -> Result<Option<libremetaverse::assets::AssetTexture>, 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<Self, crate::Error> {
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<Self, crate::Error> {
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<libremetaverse_types::compat::CancellationToken>,
) -> 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::UUID, crate::Error> {
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<libremetaverse_types::compat::CancellationToken>,
) -> Result<libremetaverse_types::UUID, crate::Error> {
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<Option<libremetaverse::InventoryNode>, 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<Self, crate::Error> {
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<Self, crate::Error> {
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<Self, crate::Error> {
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::BakeType, crate::Error> {
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<Option<libremetaverse_imaging::ManagedImage>, 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::assets::AssetTexture> {
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::AppearanceManagerTextureData> {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Imaging.Baker.Textures")
self.native_textures()
}
}

View File

@@ -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<Self, Error> {
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<InventoryFolder>,
items: Vec<InventoryBase>,
}
impl InitialOutfit {
#[allow(clippy::unnecessary_wraps)] // The generated constructor uses the crate Result model.
pub(crate) fn native_new(client: GridClient, cof: CurrentOutfitFolder) -> Result<Self, Error> {
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<Option<InventoryNode>, 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<CancellationToken>,
) -> Result<UUID, Error> {
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<CancellationToken>,
) -> 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<FolderPlan>, 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::<InventoryFolder>() {
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<CancellationToken>,
progress: Option<Box<dyn IProgress<InitialOutfitProgress>>>,
) -> Result<UUID, Error> {
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<CancellationToken>,
progress: Option<Box<dyn IProgress<InitialOutfitProgress>>>,
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::<InventoryFolder>(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<InitialOutfitProgress>>,
) -> 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<CancellationToken>,
progress: Option<Box<dyn IProgress<InitialOutfitProgress>>>,
) -> 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::<InventoryFolder>().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::<dyn IProgress<InitialOutfitProgress>>::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<InitialOutfitProgress>>,
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<dyn IProgress<InitialOutfitProgress>>);
impl IProgress<InitialOutfitProgress> for SharedProgress {
fn report(&self, value: InitialOutfitProgress) {
self.0.report(value);
}
}
pub struct InitialOutfitHandler {
cancellation: CancellationTokenSource,
subscription: Mutex<Option<Subscription>>,
}
impl InitialOutfitHandler {
pub(crate) fn native_new(
client: GridClient,
cof: CurrentOutfitFolder,
progress: Option<Box<dyn IProgress<InitialOutfitProgress>>>,
) -> Result<Self, Error> {
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::<dyn IProgress<InitialOutfitProgress>>::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());
}
}

View File

@@ -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<Option<libremetaverse_imaging::ManagedImage>, 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)
}
}