Files
MetaCrate/tools/codegen/src/xml_catalog.rs
Chili Palmer 43820f6bd1
All checks were successful
Native code generation / deterministic (push) Successful in 14m42s
Imaging and meshing gate / native (push) Successful in 4m59s
Native Rust workspace compile / compile (push) Successful in 5m7s
Implement native object models (#68)
2026-08-10 13:32:08 +00:00

1104 lines
48 KiB
Rust

//! 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<String>,
label: String,
label_min: String,
label_max: String,
default_value: f32,
min_value: f32,
max_value: f32,
is_bump_attribute: bool,
alpha: Option<AlphaDefinition>,
color: Option<ColorDefinition>,
driven: Vec<DrivenDefinition>,
skeletal: Vec<SkeletalDefinition>,
volume_morphs: Vec<VolumeMorphDefinition>,
}
#[derive(Clone, Debug, PartialEq)]
struct VisualCatalog {
params: BTreeMap<i32, VisualDefinition>,
transmitted_ids: Vec<i32>,
}
#[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<String, String> {
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<i32, String> {
let value = required_attribute(path, node, name, code)?;
value.parse::<i32>().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<u8, String> {
let value = required_attribute(path, node, name, code)?;
value.parse::<u8>().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<f32, String> {
let value = required_attribute(path, node, name, code)?;
let parsed = value.parse::<f32>().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<f32, String> {
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::<f32>)
.collect::<Result<Vec<_>, _>>()
.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::<u8>)
.collect::<Result<Vec<_>, _>>()
.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<VisualCatalog, String> {
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 <linden_avatar>",
));
}
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::<i32>().map_err(|_| {
location(
path,
node,
"VP012",
&format!(
"attribute \"group\" must be a signed 32-bit integer, found {group_text:?}"
),
)
})?;
if params.contains_key(&param_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 <param_alpha>"));
}
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 <param_color>"));
}
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::<Result<Vec<_>, _>>()?;
if colors.is_empty() {
return Err(location(
path,
child,
"VP023",
"<param_color> requires at least one <value color=...>",
));
}
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<Vec<TreeDefinition>, 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 <tree_defs>",
));
}
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",
"<tree_defs> may contain only <tree> 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<Vec<GrassDefinition>, 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 <grass_defs>",
));
}
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",
"<grass_defs> may contain only <grass> 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<Item = u8>,
) -> Result<(), String> {
let ids = ids.collect::<Vec<_>>();
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<Self, crate::Error> { Ok(Self { domain, multiply_blend, skip_if_zero, tga_file }) } }\n\n\
#[derive(Clone, Debug, PartialEq)]\n\
pub struct VisualColorParam { pub colors: Vec<libremetaverse_types::Color4>, pub operation: crate::VisualColorOperation }\n\
impl VisualColorParam { pub fn new(operation: crate::VisualColorOperation, colors: Vec<libremetaverse_types::Color4>) -> Result<Self, crate::Error> { 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<Self, crate::Error> { 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<Self, crate::Error> { 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<Self, crate::Error> { 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<Option<VisualAlphaParam>>, pub color_params: Option<Option<VisualColorParam>>,\n\
pub default_value: f32, pub driven_params: Option<Vec<DrivenParamInfo>>, pub drivers: Option<Vec<i32>>,\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<Vec<SkeletalBoneInfo>>, pub volume_morphs: Option<Vec<VolumeMorphInfo>>, pub wearable: Option<String>,\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<String>, label: String, label_min: String, label_max: String, def_: f32, min: f32, max: f32, is_bump_attribute: bool, drivers: Option<Vec<i32>>, alpha: Option<Option<VisualAlphaParam>>, color_params: Option<Option<VisualColorParam>>, driven_params: Option<Vec<DrivenParamInfo>>, skeletal_distortions: Option<Vec<SkeletalBoneInfo>>, volume_morphs: Option<Vec<VolumeMorphInfo>>) -> Result<Self, crate::Error> { 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<i32, VisualParam> {\n static PARAMS: OnceLock<BTreeMap<i32, VisualParam>> = 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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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<i32> { GROUP0_PARAM_IDS.to_vec() }\n\
pub fn params() -> libremetaverse_types::compat::SortedList<i32, VisualParam> { libremetaverse_types::compat::SortedList(generated_params().clone()) }\n\
pub fn find(name: String, wearable: Option<String>) -> Result<VisualParam, crate::Error> { 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<i32, f32> {\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<crate::Avatar, crate::Error> {\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 = &params[&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<Vec<TreeDefinition>> = 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<TreeDefinition> { generated_trees().to_vec() } pub fn get(species: crate::Tree) -> Result<TreeDefinition, crate::Error> { 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<Vec<GrassDefinition>> = 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<GrassDefinition> { generated_grasses().to_vec() } pub fn get(species: crate::Grass) -> Result<GrassDefinition, crate::Error> { grass_by_id(species as u8) } }\n\
pub(crate) fn grass_by_id(species: u8) -> Result<GrassDefinition, crate::Error> { 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<String, String> {
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<String, String> {
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<Vec<u8>, 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<Vec<u8>, 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 = "<linden_avatar><param id=\"1\" name=\"Bad\" group=\"0\" value_min=\"x\" value_max=\"1\"/></linden_avatar>";
assert!(
parse_visual_catalog("bad.xml", malformed)
.unwrap_err()
.contains("error[VP014]")
);
let missing = "<linden_avatar><param id=\"1\" name=\"Driver\" group=\"0\" value_min=\"0\" value_max=\"1\"><param_driver><driven id=\"2\"/></param_driver></param></linden_avatar>";
assert!(
parse_visual_catalog("bad.xml", missing)
.unwrap_err()
.contains("error[VP061]")
);
}
#[test]
fn foliage_schema_rejects_noncontiguous_ids_and_bad_uuids() {
let tree = "<tree_defs><tree name=\"Bad\" species_id=\"1\" texture_id=\"bad\" droop=\"0\" twist=\"0\" branches=\"0\" depth=\"0\" scale_step=\"0\" trunk_depth=\"0\" branch_length=\"0\" trunk_length=\"0\" leaf_scale=\"0\" billboard_scale=\"0\" billboard_ratio=\"0\" trunk_aspect=\"0\" branch_aspect=\"0\" leaf_rotate=\"0\" noise_mag=\"0\" noise_scale=\"0\" taper=\"0\" repeat_z=\"0\"/></tree_defs>";
assert!(
parse_trees("bad.xml", tree)
.unwrap_err()
.contains("error[TR006]")
);
let grass = "<grass_defs><grass name=\"Bad\" species_id=\"1\" texture_id=\"00000000-0000-0000-0000-000000000000\" blade_size_x=\"1\" blade_size_y=\"1\"/></grass_defs>";
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"
);
}
}