//! Schema-validated generators for the pinned avatar and foliage XML catalogs. use super::{InputSpec, generated_rust, normalize_text, verify_inputs}; use roxmltree::{Document, Node}; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use std::fs; use std::path::Path; #[derive(Clone, Debug, PartialEq)] struct AlphaDefinition { domain: f32, tga_file: String, skip_if_zero: bool, multiply_blend: bool, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ColorOperation { Add, Blend, } #[derive(Clone, Debug, PartialEq)] struct ColorDefinition { operation: ColorOperation, colors: Vec<[u8; 4]>, } #[derive(Clone, Debug, PartialEq)] struct DrivenDefinition { param_id: i32, min1: f32, max1: f32, max2: f32, min2: f32, has_range: bool, } #[derive(Clone, Debug, PartialEq)] struct SkeletalDefinition { bone_name: String, scale: [f32; 3], position: [f32; 3], has_position: bool, } #[derive(Clone, Debug, PartialEq)] struct VolumeMorphDefinition { bone_name: String, scale: [f32; 3], has_scale: bool, position: [f32; 3], has_position: bool, } #[derive(Clone, Debug, PartialEq)] struct VisualDefinition { param_id: i32, name: String, group: i32, wearable: Option, label: String, label_min: String, label_max: String, default_value: f32, min_value: f32, max_value: f32, is_bump_attribute: bool, alpha: Option, color: Option, driven: Vec, skeletal: Vec, volume_morphs: Vec, } #[derive(Clone, Debug, PartialEq)] struct VisualCatalog { params: BTreeMap, transmitted_ids: Vec, } #[derive(Clone, Debug, PartialEq)] struct TreeDefinition { name: String, species_id: u8, texture_id: String, droop: f32, twist: f32, branches: f32, depth: i32, scale_step: f32, trunk_depth: f32, branch_length: f32, trunk_length: f32, leaf_scale: f32, billboard_scale: f32, billboard_ratio: f32, trunk_aspect: f32, branch_aspect: f32, leaf_rotate: f32, noise_mag: f32, noise_scale: f32, taper: f32, repeat_z: i32, } #[derive(Clone, Debug, PartialEq)] struct GrassDefinition { name: String, species_id: u8, texture_id: String, blade_size_x: f32, blade_size_y: f32, } fn location(path: &str, node: Node<'_, '_>, code: &str, message: &str) -> String { let position = node.document().text_pos_at(node.range().start); format!( "{path}:{}:{}: error[{code}]: {message}", position.row, position.col ) } fn required_attribute( path: &str, node: Node<'_, '_>, name: &str, code: &str, ) -> Result { node.attribute(name).map(str::to_owned).ok_or_else(|| { location( path, node, code, &format!("<{}> requires attribute {name:?}", node.tag_name().name()), ) }) } fn parse_i32(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result { let value = required_attribute(path, node, name, code)?; value.parse::().map_err(|_| { location( path, node, code, &format!("attribute {name:?} must be a signed 32-bit integer, found {value:?}"), ) }) } fn parse_u8(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result { let value = required_attribute(path, node, name, code)?; value.parse::().map_err(|_| { location( path, node, code, &format!("attribute {name:?} must be an unsigned byte, found {value:?}"), ) }) } fn parse_f32(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result { let value = required_attribute(path, node, name, code)?; let parsed = value.parse::().map_err(|_| { location( path, node, code, &format!("attribute {name:?} must be a finite float, found {value:?}"), ) })?; if parsed.is_finite() { Ok(parsed) } else { Err(location( path, node, code, &format!("attribute {name:?} must be a finite float, found {value:?}"), )) } } fn optional_f32( path: &str, node: Node<'_, '_>, name: &str, default: f32, code: &str, ) -> Result { if node.attribute(name).is_some() { parse_f32(path, node, name, code) } else { Ok(default) } } fn true_attribute(node: Node<'_, '_>, name: &str) -> bool { node.attribute(name) .is_some_and(|value| value.eq_ignore_ascii_case("true")) } fn parse_vector( path: &str, node: Node<'_, '_>, name: &str, code: &str, ) -> Result<[f32; 3], String> { let value = required_attribute(path, node, name, code)?; let components = value .split_ascii_whitespace() .map(str::parse::) .collect::, _>>() .map_err(|_| { location( path, node, code, &format!("attribute {name:?} must contain three finite floats, found {value:?}"), ) })?; if components.len() != 3 || components.iter().any(|component| !component.is_finite()) { return Err(location( path, node, code, &format!("attribute {name:?} must contain three finite floats, found {value:?}"), )); } Ok([components[0], components[1], components[2]]) } fn parse_color(path: &str, node: Node<'_, '_>) -> Result<[u8; 4], String> { let value = required_attribute(path, node, "color", "VP021")?; let components = value .split(|character: char| character == ',' || character.is_ascii_whitespace()) .filter(|component| !component.is_empty()) .map(str::parse::) .collect::, _>>() .map_err(|_| { location( path, node, "VP021", &format!("color must contain four bytes, found {value:?}"), ) })?; if components.len() != 4 { return Err(location( path, node, "VP021", &format!("color must contain four bytes, found {value:?}"), )); } Ok([components[0], components[1], components[2], components[3]]) } #[allow(clippy::too_many_lines)] // The schema mirrors all golden visual-param child variants. fn parse_visual_catalog(path: &str, text: &str) -> Result { let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?; let root = document.root_element(); if !root.has_tag_name("linden_avatar") { return Err(location( path, root, "VP001", "root element must be ", )); } let mut params = BTreeMap::new(); let mut transmitted_ids = Vec::new(); for node in root .descendants() .filter(|node| node.is_element() && node.has_tag_name("param")) { let Some(group_text) = node.attribute("group") else { continue; }; if node.attribute("shared") == Some("1") { continue; } let param_id = parse_i32(path, node, "id", "VP010")?; let name = required_attribute(path, node, "name", "VP011")?; let group = group_text.parse::().map_err(|_| { location( path, node, "VP012", &format!( "attribute \"group\" must be a signed 32-bit integer, found {group_text:?}" ), ) })?; if params.contains_key(¶m_id) { return Err(location( path, node, "VP013", &format!("duplicate visual parameter ID {param_id}"), )); } let min_value = parse_f32(path, node, "value_min", "VP014")?; let max_value = parse_f32(path, node, "value_max", "VP015")?; if min_value > max_value { return Err(location( path, node, "VP016", &format!("visual parameter {param_id} has value_min greater than value_max"), )); } let default_value = optional_f32(path, node, "value_default", 0.0, "VP017")?; let parent_layer = node.parent().filter(|parent| parent.has_tag_name("layer")); let is_bump_attribute = parent_layer.and_then(|parent| parent.attribute("render_pass")) == Some("bump"); let skip_color = parent_layer.is_some_and(|parent| { parent.children().any(|child| { child.has_tag_name("texture") && child .attribute("local_texture_alpha_only") .is_some_and(|value| value.eq_ignore_ascii_case("true")) }) }); let mut alpha = None; let mut color = None; let mut driven = Vec::new(); let mut skeletal = Vec::new(); let mut volume_morphs = Vec::new(); for child in node.children().filter(Node::is_element) { if child.has_tag_name("param_alpha") { if alpha.is_some() { return Err(location(path, child, "VP018", "duplicate ")); } alpha = Some(AlphaDefinition { domain: optional_f32(path, child, "domain", 0.0, "VP019")?, tga_file: child.attribute("tga_file").unwrap_or_default().to_owned(), skip_if_zero: true_attribute(child, "skip_if_zero"), multiply_blend: true_attribute(child, "multiply_blend"), }); } else if child.has_tag_name("param_color") { if color.is_some() { return Err(location(path, child, "VP020", "duplicate ")); } let operation = match child.attribute("operation") { None => ColorOperation::Add, Some("blend" | "multiply") => ColorOperation::Blend, Some(value) => { return Err(location( path, child, "VP022", &format!("unknown color operation {value:?}"), )); } }; let colors = child .children() .filter(|value| value.is_element() && value.has_tag_name("value")) .map(|value| parse_color(path, value)) .collect::, _>>()?; if colors.is_empty() { return Err(location( path, child, "VP023", " requires at least one ", )); } if !skip_color { color = Some(ColorDefinition { operation, colors }); } } else if child.has_tag_name("param_driver") { for driven_node in child .children() .filter(|value| value.is_element() && value.has_tag_name("driven")) { let driven_id = parse_i32(path, driven_node, "id", "VP030")?; let has_range = driven_node.attribute("min1").is_some(); let item = if has_range { DrivenDefinition { param_id: driven_id, min1: parse_f32(path, driven_node, "min1", "VP031")?, max1: parse_f32(path, driven_node, "max1", "VP032")?, max2: parse_f32(path, driven_node, "max2", "VP033")?, min2: parse_f32(path, driven_node, "min2", "VP034")?, has_range: true, } } else { for attribute in ["max1", "max2", "min2"] { if driven_node.attribute(attribute).is_some() { return Err(location( path, driven_node, "VP035", "driven range attributes must be supplied together", )); } } DrivenDefinition { param_id: driven_id, min1: 0.0, max1: 0.0, max2: 0.0, min2: 0.0, has_range: false, } }; driven.push(item); } } else if child.has_tag_name("param_skeleton") { for bone in child .children() .filter(|value| value.is_element() && value.has_tag_name("bone")) { let position = bone .attribute("offset") .map(|_| parse_vector(path, bone, "offset", "VP041")) .transpose()? .unwrap_or([0.0; 3]); skeletal.push(SkeletalDefinition { bone_name: required_attribute(path, bone, "name", "VP040")?, scale: parse_vector(path, bone, "scale", "VP042")?, position, has_position: bone.attribute("offset").is_some(), }); } } else if child.has_tag_name("param_morph") { for morph in child .children() .filter(|value| value.is_element() && value.has_tag_name("volume_morph")) { let scale = morph .attribute("scale") .map(|_| parse_vector(path, morph, "scale", "VP051")) .transpose()? .unwrap_or([0.0; 3]); let position = morph .attribute("pos") .map(|_| parse_vector(path, morph, "pos", "VP052")) .transpose()? .unwrap_or([0.0; 3]); volume_morphs.push(VolumeMorphDefinition { bone_name: required_attribute(path, morph, "name", "VP050")?, scale, has_scale: morph.attribute("scale").is_some(), position, has_position: morph.attribute("pos").is_some(), }); } } } if matches!(group, 0 | 3) { transmitted_ids.push(param_id); } params.insert( param_id, VisualDefinition { param_id, name, group, wearable: node.attribute("wearable").map(str::to_owned), label: node.attribute("label").unwrap_or_default().to_owned(), label_min: node.attribute("label_min").unwrap_or_default().to_owned(), label_max: node.attribute("label_max").unwrap_or_default().to_owned(), default_value, min_value, max_value, is_bump_attribute, alpha, color, driven, skeletal, volume_morphs, }, ); } transmitted_ids.sort_unstable(); if params.is_empty() || transmitted_ids.is_empty() { return Err(location( path, root, "VP060", "visual parameter catalog must not be empty", )); } for definition in params.values() { for driven in &definition.driven { if !params.contains_key(&driven.param_id) { return Err(location( path, root, "VP061", &format!( "visual parameter {} drives missing parameter {}", definition.param_id, driven.param_id ), )); } } } Ok(VisualCatalog { params, transmitted_ids, }) } fn valid_uuid(value: &str) -> bool { value.len() == 36 && value.bytes().enumerate().all(|(index, byte)| match index { 8 | 13 | 18 | 23 => byte == b'-', _ => byte.is_ascii_hexdigit(), }) } fn parse_trees(path: &str, text: &str) -> Result, String> { let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?; let root = document.root_element(); if !root.has_tag_name("tree_defs") { return Err(location( path, root, "TR001", "root element must be ", )); } let mut definitions = Vec::new(); let mut ids = BTreeSet::new(); for node in root.children().filter(Node::is_element) { if !node.has_tag_name("tree") { return Err(location( path, node, "TR002", " may contain only elements", )); } let species_id = parse_u8(path, node, "species_id", "TR003")?; if !ids.insert(species_id) { return Err(location( path, node, "TR004", &format!("duplicate tree species ID {species_id}"), )); } let texture_id = required_attribute(path, node, "texture_id", "TR005")?; if !valid_uuid(&texture_id) { return Err(location( path, node, "TR006", &format!("invalid tree texture UUID {texture_id:?}"), )); } definitions.push(TreeDefinition { name: required_attribute(path, node, "name", "TR007")?, species_id, texture_id, droop: parse_f32(path, node, "droop", "TR010")?, twist: parse_f32(path, node, "twist", "TR011")?, branches: parse_f32(path, node, "branches", "TR012")?, depth: parse_i32(path, node, "depth", "TR013")?, scale_step: parse_f32(path, node, "scale_step", "TR014")?, trunk_depth: parse_f32(path, node, "trunk_depth", "TR015")?, branch_length: parse_f32(path, node, "branch_length", "TR016")?, trunk_length: parse_f32(path, node, "trunk_length", "TR017")?, leaf_scale: parse_f32(path, node, "leaf_scale", "TR018")?, billboard_scale: parse_f32(path, node, "billboard_scale", "TR019")?, billboard_ratio: parse_f32(path, node, "billboard_ratio", "TR020")?, trunk_aspect: parse_f32(path, node, "trunk_aspect", "TR021")?, branch_aspect: parse_f32(path, node, "branch_aspect", "TR022")?, leaf_rotate: parse_f32(path, node, "leaf_rotate", "TR023")?, noise_mag: parse_f32(path, node, "noise_mag", "TR024")?, noise_scale: parse_f32(path, node, "noise_scale", "TR025")?, taper: parse_f32(path, node, "taper", "TR026")?, repeat_z: parse_i32(path, node, "repeat_z", "TR027")?, }); } validate_contiguous( path, root, "tree", definitions.iter().map(|item| item.species_id), )?; Ok(definitions) } fn parse_grass(path: &str, text: &str) -> Result, String> { let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?; let root = document.root_element(); if !root.has_tag_name("grass_defs") { return Err(location( path, root, "GR001", "root element must be ", )); } let mut definitions = Vec::new(); let mut ids = BTreeSet::new(); for node in root.children().filter(Node::is_element) { if !node.has_tag_name("grass") { return Err(location( path, node, "GR002", " may contain only elements", )); } let species_id = parse_u8(path, node, "species_id", "GR003")?; if !ids.insert(species_id) { return Err(location( path, node, "GR004", &format!("duplicate grass species ID {species_id}"), )); } let texture_id = required_attribute(path, node, "texture_id", "GR005")?; if !valid_uuid(&texture_id) { return Err(location( path, node, "GR006", &format!("invalid grass texture UUID {texture_id:?}"), )); } definitions.push(GrassDefinition { name: required_attribute(path, node, "name", "GR007")?, species_id, texture_id, blade_size_x: parse_f32(path, node, "blade_size_x", "GR010")?, blade_size_y: parse_f32(path, node, "blade_size_y", "GR011")?, }); } validate_contiguous( path, root, "grass", definitions.iter().map(|item| item.species_id), )?; Ok(definitions) } fn validate_contiguous( path: &str, root: Node<'_, '_>, name: &str, ids: impl Iterator, ) -> Result<(), String> { let ids = ids.collect::>(); if ids.is_empty() { return Err(location( path, root, "FG020", &format!("{name} catalog must not be empty"), )); } if ids .iter() .copied() .enumerate() .any(|(index, id)| usize::from(id) != index) { return Err(location( path, root, "FG021", &format!("{name} species IDs must be contiguous and in source order from zero"), )); } Ok(()) } fn rust_f32(value: f32) -> String { format!("{value:?}_f32") } fn vector_expression(value: [f32; 3]) -> String { format!( "libremetaverse_types::Vector3 {{ x: {}, y: {}, z: {} }}", rust_f32(value[0]), rust_f32(value[1]), rust_f32(value[2]) ) } fn option_string(value: Option<&str>) -> String { value.map_or_else( || "None".to_owned(), |value| format!("Some({value:?}.to_owned())"), ) } #[allow(clippy::too_many_lines)] // One renderer keeps the generated public type/data contract atomic. fn render_visual_catalog(catalog: &VisualCatalog) -> String { let mut body = String::new(); body.push_str( "#![allow(clippy::float_cmp, clippy::manual_string_new, clippy::missing_errors_doc, clippy::must_use_candidate, clippy::needless_pass_by_value, clippy::too_many_lines, clippy::unnecessary_wraps)]\n\n\ use std::collections::{BTreeMap, HashMap};\n\ use std::sync::OnceLock;\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct VisualAlphaParam { pub domain: f32, pub multiply_blend: bool, pub skip_if_zero: bool, pub tga_file: String }\n\ impl VisualAlphaParam { pub fn new(domain: f32, tga_file: String, skip_if_zero: bool, multiply_blend: bool) -> Result { Ok(Self { domain, multiply_blend, skip_if_zero, tga_file }) } }\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct VisualColorParam { pub colors: Vec, pub operation: crate::VisualColorOperation }\n\ impl VisualColorParam { pub fn new(operation: crate::VisualColorOperation, colors: Vec) -> Result { Ok(Self { colors, operation }) } }\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct DrivenParamInfo { pub has_range: bool, pub max1: f32, pub max2: f32, pub min1: f32, pub min2: f32, pub param_id: i32 }\n\ impl DrivenParamInfo { pub fn new(param_id: i32, min1: f32, max1: f32, max2: f32, min2: f32, has_range: bool) -> Result { Ok(Self { has_range, max1, max2, min1, min2, param_id }) } }\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct SkeletalBoneInfo { pub bone_name: String, pub has_position_deformation: bool, pub position_deformation: libremetaverse_types::Vector3, pub scale_deformation: libremetaverse_types::Vector3 }\n\ impl SkeletalBoneInfo { pub fn new(bone_name: String, scale_deformation: libremetaverse_types::Vector3, position_deformation: libremetaverse_types::Vector3, has_position_deformation: bool) -> Result { Ok(Self { bone_name, has_position_deformation, position_deformation, scale_deformation }) } }\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct VolumeMorphInfo { pub bone_name: String, pub has_position: bool, pub has_scale: bool, pub position_delta: libremetaverse_types::Vector3, pub scale_delta: libremetaverse_types::Vector3 }\n\ impl VolumeMorphInfo { pub fn new(bone_name: String, scale_delta: libremetaverse_types::Vector3, has_scale: bool, position_delta: libremetaverse_types::Vector3, has_position: bool) -> Result { Ok(Self { bone_name, has_position, has_scale, position_delta, scale_delta }) } }\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct VisualParam {\n\ pub alpha_params: Option>, pub color_params: Option>,\n\ pub default_value: f32, pub driven_params: Option>, pub drivers: Option>,\n\ pub group: i32, pub is_bump_attribute: bool, pub label: String, pub label_max: String, pub label_min: String,\n\ pub max_value: f32, pub min_value: f32, pub name: String, pub param_id: i32,\n\ pub skeletal_distortions: Option>, pub volume_morphs: Option>, pub wearable: Option,\n\ }\n\ impl Default for VisualParam { fn default() -> Self { Self { alpha_params: None, color_params: None, default_value: 0.0, driven_params: None, drivers: None, group: 0, is_bump_attribute: false, label: String::new(), label_max: String::new(), label_min: String::new(), max_value: 0.0, min_value: 0.0, name: String::new(), param_id: 0, skeletal_distortions: None, volume_morphs: None, wearable: None } } }\n\ impl VisualParam {\n\ #[allow(clippy::too_many_arguments)]\n\ pub fn new(param_id: i32, name: String, group: i32, wearable: Option, label: String, label_min: String, label_max: String, def_: f32, min: f32, max: f32, is_bump_attribute: bool, drivers: Option>, alpha: Option>, color_params: Option>, driven_params: Option>, skeletal_distortions: Option>, volume_morphs: Option>) -> Result { Ok(Self { alpha_params: alpha, color_params, default_value: def_, driven_params, drivers, group, is_bump_attribute, label, label_max, label_min, max_value: max, min_value: min, name, param_id, skeletal_distortions, volume_morphs, wearable }) }\n\ }\n\n", ); body.push_str("const GROUP0_PARAM_IDS: &[i32] = &["); for id in &catalog.transmitted_ids { let _ = write!(body, "{id},"); } body.push_str("];\n\nfn generated_params() -> &'static BTreeMap {\n static PARAMS: OnceLock> = OnceLock::new();\n PARAMS.get_or_init(|| {\n let mut params = BTreeMap::new();\n"); for definition in catalog.params.values() { let alpha = definition.alpha.as_ref().map_or_else( || "None".to_owned(), |alpha| { format!( "Some(Some(VisualAlphaParam {{ domain: {}, multiply_blend: {}, skip_if_zero: {}, tga_file: {:?}.to_owned() }}))", rust_f32(alpha.domain), alpha.multiply_blend, alpha.skip_if_zero, alpha.tga_file ) }, ); let color = definition.color.as_ref().map_or_else( || "None".to_owned(), |color| { let operation = match color.operation { ColorOperation::Add => "crate::VisualColorOperation::Add", ColorOperation::Blend => "crate::VisualColorOperation::Blend", }; let values = color .colors .iter() .map(|value| { format!( "libremetaverse_types::Color4 {{ r: f32::from({}_u8) / 255.0, g: f32::from({}_u8) / 255.0, b: f32::from({}_u8) / 255.0, a: f32::from({}_u8) / 255.0 }}", value[0], value[1], value[2], value[3] ) }) .collect::>() .join(","); format!("Some(Some(VisualColorParam {{ colors: vec![{values}], operation: {operation} }}))") }, ); let drivers = if definition.driven.is_empty() { "None".to_owned() } else { format!( "Some(vec![{}])", definition .driven .iter() .map(|item| item.param_id.to_string()) .collect::>() .join(",") ) }; let driven = if definition.driven.is_empty() { "None".to_owned() } else { format!( "Some(vec![{}])", definition .driven .iter() .map(|item| format!( "DrivenParamInfo {{ has_range: {}, max1: {}, max2: {}, min1: {}, min2: {}, param_id: {} }}", item.has_range, rust_f32(item.max1), rust_f32(item.max2), rust_f32(item.min1), rust_f32(item.min2), item.param_id )) .collect::>() .join(",") ) }; let skeletal = if definition.skeletal.is_empty() { "None".to_owned() } else { format!( "Some(vec![{}])", definition .skeletal .iter() .map(|item| format!( "SkeletalBoneInfo {{ bone_name: {:?}.to_owned(), has_position_deformation: {}, position_deformation: {}, scale_deformation: {} }}", item.bone_name, item.has_position, vector_expression(item.position), vector_expression(item.scale) )) .collect::>() .join(",") ) }; let volume_morphs = if definition.volume_morphs.is_empty() { "None".to_owned() } else { format!( "Some(vec![{}])", definition .volume_morphs .iter() .map(|item| format!( "VolumeMorphInfo {{ bone_name: {:?}.to_owned(), has_position: {}, has_scale: {}, position_delta: {}, scale_delta: {} }}", item.bone_name, item.has_position, item.has_scale, vector_expression(item.position), vector_expression(item.scale) )) .collect::>() .join(",") ) }; let _ = writeln!( body, " params.insert({}, VisualParam {{ alpha_params: {alpha}, color_params: {color}, default_value: {}, driven_params: {driven}, drivers: {drivers}, group: {}, is_bump_attribute: {}, label: {:?}.to_owned(), label_max: {:?}.to_owned(), label_min: {:?}.to_owned(), max_value: {}, min_value: {}, name: {:?}.to_owned(), param_id: {}, skeletal_distortions: {skeletal}, volume_morphs: {volume_morphs}, wearable: {} }});", definition.param_id, rust_f32(definition.default_value), definition.group, definition.is_bump_attribute, definition.label, definition.label_max, definition.label_min, rust_f32(definition.max_value), rust_f32(definition.min_value), definition.name, definition.param_id, option_string(definition.wearable.as_deref()), ); } body.push_str( " params\n })\n}\n\n\ pub struct VisualParams;\n\ impl VisualParams {\n\ pub fn group0_param_ids() -> Vec { GROUP0_PARAM_IDS.to_vec() }\n\ pub fn params() -> libremetaverse_types::compat::SortedList { libremetaverse_types::compat::SortedList(generated_params().clone()) }\n\ pub fn find(name: String, wearable: Option) -> Result { Ok(generated_params().values().find(|param| param.name == name && param.wearable == wearable).cloned().unwrap_or_default()) }\n\ }\n\n\ pub(crate) fn decode_visual_params(bytes: &[u8]) -> HashMap {\n\ let params = generated_params();\n\ let mut result = HashMap::with_capacity(GROUP0_PARAM_IDS.len());\n\ for (&id, &value) in GROUP0_PARAM_IDS.iter().zip(bytes) { if let Some(param) = params.get(&id) { let range = param.max_value - param.min_value; let mut decoded = f32::from(value) * (1.0 / 255.0) * range + param.min_value; if decoded.abs() < range * (1.0 / 255.0) { decoded = 0.0; } result.insert(id, decoded); } }\n\ for driver in params.values() {\n\ let Some(driven_params) = &driver.driven_params else { continue };\n\ let Some(&driver_value) = result.get(&driver.param_id) else { continue };\n\ for driven in driven_params {\n\ let Some(target) = params.get(&driven.param_id) else { continue };\n\ let normalized = if driven.has_range {\n\ if driver_value < driven.min1 { 0.0 } else if driver_value < driven.max1 { (driver_value - driven.min1) / (driven.max1 - driven.min1) } else if driver_value <= driven.max2 { 1.0 } else if driver_value < driven.min2 { (driven.min2 - driver_value) / (driven.min2 - driven.max2) } else { 0.0 }\n\ } else { let range = driver.max_value - driver.min_value; if range > 1.0e-6 { (driver_value - driver.min_value) / range } else { 0.0 } }.clamp(0.0, 1.0);\n\ result.insert(driven.param_id, target.min_value + normalized * (target.max_value - target.min_value));\n\ }\n\ }\n\ result\n\ }\n", ); body.push_str( "pub(crate) fn new_avatar() -> Result {\n\ Ok(crate::Avatar {\n\ animations: Vec::new(), appearance_flags: crate::AppearanceFlags::NONE, appearance_version: 0,\n\ attachments: Vec::new(), cof_version: 0, control_flags: crate::AgentManagerControlFlags(0),\n\ groups: Vec::new(), hover_height: libremetaverse_types::Vector3::zero(),\n\ profile_interests: crate::AvatarInterests { languages_text: String::new(), skills_mask: 0, skills_text: String::new(), want_to_mask: 0, want_to_text: String::new() },\n\ profile_properties: crate::AvatarAvatarProperties { about_text: String::new(), born_on: String::new(), charter_member: String::new(), first_life_image: libremetaverse_types::UUID::zero(), first_life_text: String::new(), flags: crate::ProfileFlags(0), partner: libremetaverse_types::UUID::zero(), profile_image: libremetaverse_types::UUID::zero(), profile_url: String::new() },\n\ profile_statistics: crate::AvatarStatistics { appearance_negative: 0, appearance_positive: 0, behavior_negative: 0, behavior_positive: 0, building_negative: 0, building_positive: 0, given_negative: 0, given_positive: 0 },\n\ visual_parameters: Vec::new(),\n\ })\n\ }\n\n\ #[cfg(test)] mod tests { use super::*; #[test] fn golden_catalog_shape_and_values() { let params = generated_params(); assert_eq!(params.len(), 672); assert_eq!(GROUP0_PARAM_IDS.len(), 253); assert!(GROUP0_PARAM_IDS.windows(2).all(|ids| ids[0] < ids[1])); let height = ¶ms[&33]; assert_eq!(height.name, \"Height\"); assert_eq!(height.group, 0); assert_eq!(height.wearable.as_deref(), Some(\"shape\")); assert_eq!(height.min_value, -2.3); assert_eq!(height.max_value, 2.0); assert!(!height.skeletal_distortions.as_ref().unwrap().is_empty()); } }\n", ); body } fn render_foliage_catalog(trees: &[TreeDefinition], grasses: &[GrassDefinition]) -> String { let mut body = String::new(); body.push_str( "#![allow(clippy::missing_errors_doc, clippy::must_use_candidate, clippy::too_many_lines)]\n\n\ use std::sync::OnceLock;\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct TreeDefinition { name: String, species_id: i32, texture_id: libremetaverse_types::UUID, droop: f32, twist: f32, branches: f32, depth: i32, scale_step: f32, trunk_depth: f32, branch_length: f32, trunk_length: f32, leaf_scale: f32, billboard_scale: f32, billboard_ratio: f32, trunk_aspect: f32, branch_aspect: f32, leaf_rotate: f32, noise_mag: f32, noise_scale: f32, taper: f32, repeat_z: i32 }\n\ impl TreeDefinition {\n\ pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; }\n\ pub fn species_id(&self) -> i32 { self.species_id } pub fn set_species_id(&mut self, value: i32) { self.species_id = value; }\n\ pub fn texture_id(&self) -> libremetaverse_types::UUID { self.texture_id } pub fn set_texture_id(&mut self, value: libremetaverse_types::UUID) { self.texture_id = value; }\n", ); for name in [ "droop", "twist", "branches", "scale_step", "trunk_depth", "branch_length", "trunk_length", "leaf_scale", "billboard_scale", "billboard_ratio", "trunk_aspect", "branch_aspect", "leaf_rotate", "noise_mag", "noise_scale", "taper", ] { let _ = writeln!( body, " pub fn {name}(&self) -> f32 {{ self.{name} }} pub fn set_{name}(&mut self, value: f32) {{ self.{name} = value; }}" ); } body.push_str(" pub fn depth(&self) -> i32 { self.depth } pub fn set_depth(&mut self, value: i32) { self.depth = value; }\n pub fn repeat_z(&self) -> i32 { self.repeat_z } pub fn set_repeat_z(&mut self, value: i32) { self.repeat_z = value; }\n}\n\n"); body.push_str("fn generated_trees() -> &'static [TreeDefinition] { static TREES: OnceLock> = OnceLock::new(); TREES.get_or_init(|| vec![\n"); for tree in trees { let _ = writeln!( body, " TreeDefinition {{ name: {:?}.to_owned(), species_id: {}, texture_id: libremetaverse_types::UUID::parse({:?}.to_owned()).expect(\"validated tree UUID\"), droop: {}, twist: {}, branches: {}, depth: {}, scale_step: {}, trunk_depth: {}, branch_length: {}, trunk_length: {}, leaf_scale: {}, billboard_scale: {}, billboard_ratio: {}, trunk_aspect: {}, branch_aspect: {}, leaf_rotate: {}, noise_mag: {}, noise_scale: {}, taper: {}, repeat_z: {} }},", tree.name, tree.species_id, tree.texture_id, rust_f32(tree.droop), rust_f32(tree.twist), rust_f32(tree.branches), tree.depth, rust_f32(tree.scale_step), rust_f32(tree.trunk_depth), rust_f32(tree.branch_length), rust_f32(tree.trunk_length), rust_f32(tree.leaf_scale), rust_f32(tree.billboard_scale), rust_f32(tree.billboard_ratio), rust_f32(tree.trunk_aspect), rust_f32(tree.branch_aspect), rust_f32(tree.leaf_rotate), rust_f32(tree.noise_mag), rust_f32(tree.noise_scale), rust_f32(tree.taper), tree.repeat_z ); } body.push_str( "]).as_slice() }\n\n\ pub struct TreeDefinitions; impl TreeDefinitions { pub fn all() -> Vec { generated_trees().to_vec() } pub fn get(species: crate::Tree) -> Result { generated_trees().get(species as usize).cloned().ok_or(crate::Error::Argument) } }\n\n\ #[derive(Clone, Debug, PartialEq)]\n\ pub struct GrassDefinition { name: String, species_id: i32, texture_id: libremetaverse_types::UUID, blade_size_x: f32, blade_size_y: f32 }\n\ impl GrassDefinition {\n\ pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; }\n\ pub fn species_id(&self) -> i32 { self.species_id } pub fn set_species_id(&mut self, value: i32) { self.species_id = value; }\n\ pub fn texture_id(&self) -> libremetaverse_types::UUID { self.texture_id } pub fn set_texture_id(&mut self, value: libremetaverse_types::UUID) { self.texture_id = value; }\n\ pub fn blade_size_x(&self) -> f32 { self.blade_size_x } pub fn set_blade_size_x(&mut self, value: f32) { self.blade_size_x = value; }\n\ pub fn blade_size_y(&self) -> f32 { self.blade_size_y } pub fn set_blade_size_y(&mut self, value: f32) { self.blade_size_y = value; }\n\ }\n\nfn generated_grasses() -> &'static [GrassDefinition] { static GRASSES: OnceLock> = OnceLock::new(); GRASSES.get_or_init(|| vec![\n", ); for grass in grasses { let _ = writeln!( body, " GrassDefinition {{ name: {:?}.to_owned(), species_id: {}, texture_id: libremetaverse_types::UUID::parse({:?}.to_owned()).expect(\"validated grass UUID\"), blade_size_x: {}, blade_size_y: {} }},", grass.name, grass.species_id, grass.texture_id, rust_f32(grass.blade_size_x), rust_f32(grass.blade_size_y) ); } body.push_str( "]).as_slice() }\n\n\ pub struct GrassDefinitions; impl GrassDefinitions { pub fn all() -> Vec { generated_grasses().to_vec() } pub fn get(species: crate::Grass) -> Result { grass_by_id(species as u8) } }\n\ pub(crate) fn grass_by_id(species: u8) -> Result { generated_grasses().get(usize::from(species)).cloned().ok_or(crate::Error::Argument) }\n\n\ #[cfg(test)] mod tests { use super::*; #[test] fn golden_foliage_shape_and_values() { let trees = TreeDefinitions::all(); assert_eq!(trees.len(), 21); assert_eq!(trees[0].name(), \"Pine 1\"); assert_eq!(trees[0].species_id(), crate::Tree::Pine1 as i32); assert_eq!(trees[20].name(), \"Kelp 2\"); assert_eq!(TreeDefinitions::get(crate::Tree::Oak).unwrap(), trees[1]); let grass = GrassDefinitions::all(); assert_eq!(grass.len(), 6); assert_eq!(grass[5].name(), \"undergrowth_1\"); assert_eq!(GrassDefinitions::get(crate::Grass::Undergrowth1).unwrap(), grass[5]); } }\n", ); body } fn load_input<'a>(inventory: &'a super::Inventory, id: &str) -> Result<&'a InputSpec, String> { inventory .inputs .iter() .find(|input| input.id == id) .ok_or_else(|| format!("source inventory has no {id} input")) } fn load_text(root: &Path, input: &InputSpec) -> Result { let path = root.join(&input.vendored_path); let bytes = fs::read(&path).map_err(|error| format!("{}: {error}", path.display()))?; normalize_text(&input.vendored_path, &bytes).map_err(|error| error.to_string()) } fn format_body(body: &str, name: &str) -> Result { let syntax = syn::parse_file(body) .map_err(|error| format!("generated {name} Rust is invalid: {error}"))?; Ok(prettyplease::unparse(&syntax)) } pub(super) fn visual_catalog_bytes(root: &Path) -> Result, String> { let inventory = super::load_inventory(root)?; verify_inputs(root, &inventory)?; let input = load_input(&inventory, "avatar_lad")?; let catalog = parse_visual_catalog(&input.vendored_path, &load_text(root, input)?)?; let body = format_body(&render_visual_catalog(&catalog), "visual parameter")?; Ok(generated_rust("visual-params", &[input], &body)) } pub(super) fn foliage_catalog_bytes(root: &Path) -> Result, String> { let inventory = super::load_inventory(root)?; verify_inputs(root, &inventory)?; let tree_input = load_input(&inventory, "trees")?; let grass_input = load_input(&inventory, "grass")?; let trees = parse_trees(&tree_input.vendored_path, &load_text(root, tree_input)?)?; let grasses = parse_grass(&grass_input.vendored_path, &load_text(root, grass_input)?)?; let body = format_body(&render_foliage_catalog(&trees, &grasses), "foliage")?; Ok(generated_rust("foliage", &[tree_input, grass_input], &body)) } #[cfg(test)] mod tests { use super::*; #[test] fn visual_schema_rejects_malformed_values_and_missing_driver_targets() { let malformed = ""; assert!( parse_visual_catalog("bad.xml", malformed) .unwrap_err() .contains("error[VP014]") ); let missing = ""; assert!( parse_visual_catalog("bad.xml", missing) .unwrap_err() .contains("error[VP061]") ); } #[test] fn foliage_schema_rejects_noncontiguous_ids_and_bad_uuids() { let tree = ""; assert!( parse_trees("bad.xml", tree) .unwrap_err() .contains("error[TR006]") ); let grass = ""; assert!( parse_grass("bad.xml", grass) .unwrap_err() .contains("error[FG021]") ); } #[test] fn pinned_catalog_semantics_have_stable_digests() { let root = super::super::workspace_root(); let inventory = super::super::load_inventory(&root).expect("pinned inventory"); let visual_input = load_input(&inventory, "avatar_lad").expect("visual input"); let visual = parse_visual_catalog( &visual_input.vendored_path, &load_text(&root, visual_input).expect("visual text"), ) .expect("visual catalog"); let tree_input = load_input(&inventory, "trees").expect("tree input"); let trees = parse_trees( &tree_input.vendored_path, &load_text(&root, tree_input).expect("tree text"), ) .expect("tree catalog"); let grass_input = load_input(&inventory, "grass").expect("grass input"); let grasses = parse_grass( &grass_input.vendored_path, &load_text(&root, grass_input).expect("grass text"), ) .expect("grass catalog"); assert_eq!( super::super::sha256(format!("{visual:#?}").as_bytes()), "3cdedcd412fa8f8dee4a99407968e849c55bdcdb13b883861352d042b1783a4a" ); assert_eq!( super::super::sha256(format!("{trees:#?}\n{grasses:#?}").as_bytes()), "636b3a00085d68f72c8506766a7e09c37e7db41bfca1acae228ad9cdb6b5ee50" ); } }