Generate skeleton attention and genepool catalogs (#49)
Some checks failed
Native code generation / deterministic (push) Successful in 5m4s
Imaging and meshing gate / native (push) Failing after 17s
JPEG 2000 feature / linux (push) Failing after 59s
Skia feature / linux (push) Failing after 1m31s

This commit is contained in:
2026-08-09 09:28:40 +00:00
parent 5f83ce61e1
commit 4a32025e37
18 changed files with 7018 additions and 423 deletions

View File

@@ -0,0 +1,881 @@
//! Schema-validated generators for skeleton, attention, and genepool catalogs.
use super::{InputSpec, generated_rust, normalize_text, verify_inputs};
use roxmltree::{Document, Node};
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
#[derive(Clone, Debug, PartialEq)]
struct JointBaseRecord {
name: String,
pos: Vec<f32>,
rot: Vec<f32>,
scale: Vec<f32>,
group: String,
support: String,
end: Vec<f32>,
reposition: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct CollisionRecord {
base: JointBaseRecord,
}
#[derive(Clone, Debug, PartialEq)]
struct JointRecord {
base: JointBaseRecord,
collision_volumes: Vec<CollisionRecord>,
bones: Vec<JointRecord>,
pivot: Vec<f32>,
aliases: String,
connected: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct SkeletonRecord {
version: f32,
num_bones: String,
num_collision_volumes: String,
root: JointRecord,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct AttentionRecord {
priority: f32,
timeout: f32,
}
#[derive(Clone, Debug, PartialEq)]
struct AttentionSetRecord {
name: String,
entries: [AttentionRecord; 11],
}
#[derive(Clone, Debug, PartialEq)]
struct ArchetypeParamRecord {
id: i32,
name: String,
value: f32,
}
#[derive(Clone, Debug, PartialEq)]
struct ArchetypeRecord {
name: String,
params: Vec<ArchetypeParamRecord>,
}
const ATTENTION_NAMES: [&str; 9] = [
"idle",
"auto_listen",
"freelook",
"respond",
"hover",
"conversation",
"select",
"focus",
"mouselook",
];
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)
.filter(|value| !value.is_empty())
.map(str::to_owned)
.ok_or_else(|| {
location(
path,
node,
code,
&format!("<{}> requires attribute {name:?}", node.tag_name().name()),
)
})
}
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 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_usize(path: &str, node: Node<'_, '_>, name: &str, code: &str) -> Result<usize, String> {
let value = required_attribute(path, node, name, code)?;
value.parse::<usize>().map_err(|_| {
location(
path,
node,
code,
&format!("attribute {name:?} must be a non-negative integer, found {value:?}"),
)
})
}
fn vector_attribute(
path: &str,
node: Node<'_, '_>,
name: &str,
code: &str,
) -> Result<Vec<f32>, String> {
let Some(value) = node.attribute(name) else {
return Ok(Vec::new());
};
let values = 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"),
)
})?;
if values.len() != 3 || values.iter().any(|value| !value.is_finite()) {
return Err(location(
path,
node,
code,
&format!("attribute {name:?} must contain three finite floats"),
));
}
Ok(values)
}
fn boolean_attribute(
path: &str,
node: Node<'_, '_>,
name: &str,
code: &str,
) -> Result<bool, String> {
match node.attribute(name) {
None => Ok(false),
Some(value) if value.eq_ignore_ascii_case("true") => Ok(true),
Some(value) if value.eq_ignore_ascii_case("false") => Ok(false),
Some(value) => Err(location(
path,
node,
code,
&format!("attribute {name:?} must be true or false, found {value:?}"),
)),
}
}
fn parse_joint_base(
path: &str,
node: Node<'_, '_>,
name_code: &str,
) -> Result<JointBaseRecord, String> {
let support = node.attribute("support").unwrap_or_default().to_owned();
if !support.is_empty()
&& !support.eq_ignore_ascii_case("base")
&& !support.eq_ignore_ascii_case("extended")
{
return Err(location(
path,
node,
"SK018",
&format!("unknown skeleton support category {support:?}"),
));
}
Ok(JointBaseRecord {
name: required_attribute(path, node, "name", name_code)?,
pos: vector_attribute(path, node, "pos", "SK011")?,
rot: vector_attribute(path, node, "rot", "SK012")?,
scale: vector_attribute(path, node, "scale", "SK013")?,
group: node.attribute("group").unwrap_or_default().to_owned(),
support,
end: vector_attribute(path, node, "end", "SK014")?,
reposition: boolean_attribute(path, node, "reposition", "SK015")?,
})
}
fn parse_joint(
path: &str,
node: Node<'_, '_>,
identifiers: &mut BTreeSet<String>,
collision_names: &mut BTreeSet<String>,
) -> Result<JointRecord, String> {
let base = parse_joint_base(path, node, "SK010")?;
if !identifiers.insert(base.name.clone()) {
return Err(location(
path,
node,
"SK016",
&format!("duplicate skeleton joint identifier {:?}", base.name),
));
}
let aliases = node.attribute("aliases").unwrap_or_default().to_owned();
for alias in aliases.split_ascii_whitespace() {
if !identifiers.insert(alias.to_owned()) {
return Err(location(
path,
node,
"SK016",
&format!("duplicate skeleton joint identifier {alias:?}"),
));
}
}
let mut collision_volumes = Vec::new();
let mut bones = Vec::new();
for child in node.children().filter(Node::is_element) {
if child.has_tag_name("collision_volume") {
if child.children().any(|node| node.is_element()) {
return Err(location(
path,
child,
"SK019",
"<collision_volume> may not contain child elements",
));
}
let collision = CollisionRecord {
base: parse_joint_base(path, child, "SK017")?,
};
if !collision_names.insert(collision.base.name.clone()) {
return Err(location(
path,
child,
"SK020",
&format!("duplicate collision-volume name {:?}", collision.base.name),
));
}
collision_volumes.push(collision);
} else if child.has_tag_name("bone") {
bones.push(parse_joint(path, child, identifiers, collision_names)?);
} else {
return Err(location(
path,
child,
"SK021",
"<bone> may contain only <bone> and <collision_volume> elements",
));
}
}
Ok(JointRecord {
base,
collision_volumes,
bones,
pivot: vector_attribute(path, node, "pivot", "SK022")?,
aliases,
connected: boolean_attribute(path, node, "connected", "SK023")?,
})
}
fn joint_counts(root: &JointRecord) -> (usize, usize) {
let mut bones = 1;
let mut collision_volumes = root.collision_volumes.len();
for child in &root.bones {
let (child_bones, child_collision_volumes) = joint_counts(child);
bones += child_bones;
collision_volumes += child_collision_volumes;
}
(bones, collision_volumes)
}
fn parse_skeleton(path: &str, text: &str) -> Result<SkeletonRecord, String> {
let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
let root = document.root_element();
if !root.has_tag_name("linden_skeleton") {
return Err(location(
path,
root,
"SK001",
"root element must be <linden_skeleton>",
));
}
let version = parse_f32(path, root, "version", "SK002")?;
let expected_bones = parse_usize(path, root, "num_bones", "SK003")?;
let expected_collision_volumes = parse_usize(path, root, "num_collision_volumes", "SK004")?;
let children = root.children().filter(Node::is_element).collect::<Vec<_>>();
if children.len() != 1 || !children[0].has_tag_name("bone") {
return Err(location(
path,
root,
"SK005",
"skeleton requires exactly one root <bone>",
));
}
let mut identifiers = BTreeSet::new();
let mut collision_names = BTreeSet::new();
let joint = parse_joint(path, children[0], &mut identifiers, &mut collision_names)?;
let (actual_bones, actual_collision_volumes) = joint_counts(&joint);
if actual_bones != expected_bones || actual_collision_volumes != expected_collision_volumes {
return Err(location(
path,
root,
"SK006",
&format!(
"declared skeleton counts are {expected_bones} bones/{expected_collision_volumes} collision volumes but parsed {actual_bones}/{actual_collision_volumes}"
),
));
}
Ok(SkeletonRecord {
version,
num_bones: expected_bones.to_string(),
num_collision_volumes: expected_collision_volumes.to_string(),
root: joint,
})
}
fn attention_index(name: &str) -> Option<usize> {
ATTENTION_NAMES
.iter()
.position(|candidate| candidate.eq_ignore_ascii_case(name))
.map(|index| index + 1)
}
fn parse_attentions(path: &str, text: &str) -> Result<Vec<AttentionSetRecord>, String> {
let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
let root = document.root_element();
if !root.has_tag_name("linden_attentions") {
return Err(location(
path,
root,
"AT001",
"root element must be <linden_attentions>",
));
}
let _version = parse_f32(path, root, "version", "AT002")?;
let genders = root.children().filter(Node::is_element).collect::<Vec<_>>();
if genders.len() != 2 {
return Err(location(
path,
root,
"AT003",
"attention catalog requires Masculine and Feminine gender sets",
));
}
let mut sets = Vec::new();
for (gender_index, gender) in genders.into_iter().enumerate() {
if !gender.has_tag_name("gender") {
return Err(location(
path,
gender,
"AT004",
"<linden_attentions> may contain only <gender> elements",
));
}
let name = required_attribute(path, gender, "name", "AT005")?;
let expected_name = if gender_index == 0 {
"Masculine"
} else {
"Feminine"
};
if name != expected_name {
return Err(location(
path,
gender,
"AT006",
&format!("gender set {gender_index} must be named {expected_name:?}"),
));
}
let mut entries = [AttentionRecord {
priority: 0.0,
timeout: 0.0,
}; 11];
let mut seen = BTreeSet::new();
for param in gender.children().filter(Node::is_element) {
if !param.has_tag_name("param") {
return Err(location(
path,
param,
"AT007",
"<gender> may contain only <param> elements",
));
}
let attention = required_attribute(path, param, "attention", "AT008")?;
let Some(index) = attention_index(&attention) else {
return Err(location(
path,
param,
"AT009",
&format!("unknown attention name {attention:?}"),
));
};
if !seen.insert(index) {
return Err(location(
path,
param,
"AT010",
&format!("duplicate attention name {attention:?}"),
));
}
entries[index] = AttentionRecord {
priority: parse_f32(path, param, "priority", "AT011")?,
timeout: parse_f32(path, param, "timeout", "AT012")?,
};
}
if seen.len() != ATTENTION_NAMES.len() {
return Err(location(
path,
gender,
"AT013",
"gender set must define each of the nine attention parameters exactly once",
));
}
sets.push(AttentionSetRecord { name, entries });
}
Ok(sets)
}
fn parse_genepool(path: &str, text: &str) -> Result<Vec<ArchetypeRecord>, String> {
let document = Document::parse(text).map_err(|error| format!("{path}: {error}"))?;
let root = document.root_element();
if !root.has_tag_name("linden_genepool") {
return Err(location(
path,
root,
"GP001",
"root element must be <linden_genepool>",
));
}
let _version = parse_f32(path, root, "version", "GP002")?;
let mut names = BTreeSet::new();
let mut archetypes = Vec::new();
for archetype in root.children().filter(Node::is_element) {
if !archetype.has_tag_name("archetype") {
return Err(location(
path,
archetype,
"GP003",
"<linden_genepool> may contain only <archetype> elements",
));
}
let name = required_attribute(path, archetype, "name", "GP004")?
.trim()
.to_owned();
if name.is_empty() || !names.insert(name.clone()) {
return Err(location(
path,
archetype,
"GP005",
&format!("empty or duplicate archetype name {name:?}"),
));
}
let mut ids = BTreeSet::new();
let mut texture_slots = BTreeSet::new();
let mut params = Vec::new();
for child in archetype.children().filter(Node::is_element) {
if child.has_tag_name("texture") {
let slot = parse_i32(path, child, "te", "GP013")?;
let uuid = required_attribute(path, child, "uuid", "GP014")?;
if !texture_slots.insert(slot) {
return Err(location(
path,
child,
"GP015",
&format!("duplicate texture-entry slot {slot} in archetype {name:?}"),
));
}
if !valid_uuid(&uuid) {
return Err(location(
path,
child,
"GP016",
&format!("invalid archetype texture UUID {uuid:?}"),
));
}
continue;
}
if !child.has_tag_name("param") {
return Err(location(
path,
child,
"GP006",
"<archetype> may contain only <param> and <texture> elements",
));
}
let id = parse_i32(path, child, "id", "GP007")?;
if !ids.insert(id) {
return Err(location(
path,
child,
"GP008",
&format!("duplicate parameter ID {id} in archetype {name:?}"),
));
}
params.push(ArchetypeParamRecord {
id,
name: required_attribute(path, child, "name", "GP009")?,
value: parse_f32(path, child, "value", "GP010")?,
});
}
if params.is_empty() {
return Err(location(
path,
archetype,
"GP011",
"archetype must contain at least one parameter",
));
}
archetypes.push(ArchetypeRecord { name, params });
}
if archetypes.is_empty() {
return Err(location(
path,
root,
"GP012",
"genepool must contain at least one archetype",
));
}
Ok(archetypes)
}
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 rust_f32(value: f32) -> String {
format!("{value:?}_f32")
}
fn vector_expression(values: &[f32]) -> String {
format!(
"vec![{}]",
values
.iter()
.map(|value| rust_f32(*value))
.collect::<Vec<_>>()
.join(",")
)
}
fn base_expression(base: &JointBaseRecord) -> String {
format!(
"crate::skeleton::JointBase::from_parts({:?}.to_owned(), {}, {}, {}, {:?}.to_owned(), {:?}.to_owned(), {}, {})",
base.name,
vector_expression(&base.pos),
vector_expression(&base.rot),
vector_expression(&base.scale),
base.group,
base.support,
vector_expression(&base.end),
base.reposition
)
}
fn joint_expression(joint: &JointRecord) -> String {
let collision_volumes = joint
.collision_volumes
.iter()
.map(|volume| {
format!(
"crate::skeleton::CollisionVolume::from_base({})",
base_expression(&volume.base)
)
})
.collect::<Vec<_>>()
.join(",");
let bones = joint
.bones
.iter()
.map(joint_expression)
.collect::<Vec<_>>()
.join(",");
format!(
"crate::skeleton::Joint::from_parts({}, vec![{collision_volumes}], vec![{bones}], {}, {:?}.to_owned(), {})",
base_expression(&joint.base),
vector_expression(&joint.pivot),
joint.aliases,
joint.connected
)
}
fn render_skeleton(skeleton: &SkeletonRecord) -> String {
format!(
"#![allow(clippy::float_cmp, clippy::too_many_lines, clippy::unreadable_literal)]\n\n\
pub(crate) fn default_skeleton() -> crate::skeleton::LindenSkeleton {{\n\
crate::skeleton::LindenSkeleton::from_parts({}, {}, false, {:?}.to_owned(), {:?}.to_owned())\n\
}}\n\n\
#[cfg(test)] mod tests {{ use super::*; #[test] fn generated_default_shape() {{ let skeleton = default_skeleton(); assert_eq!(skeleton.version(), 2.0); assert_eq!(skeleton.num_bones(), \"133\"); assert_eq!(skeleton.num_collision_volumes(), \"26\"); assert_eq!(skeleton.bone().base.name(), \"mPelvis\"); }} }}\n",
joint_expression(&skeleton.root),
rust_f32(skeleton.version),
skeleton.num_bones,
skeleton.num_collision_volumes
)
}
fn render_attentions(default: &[AttentionSetRecord], updated: &[AttentionSetRecord]) -> String {
fn sets_expression(sets: &[AttentionSetRecord]) -> String {
sets.iter()
.map(|set| {
let entries = set
.entries
.iter()
.map(|entry| {
format!(
"AttentionData {{ priority: {}, timeout: {} }}",
rust_f32(entry.priority),
rust_f32(entry.timeout)
)
})
.collect::<Vec<_>>()
.join(",");
format!(
"AttentionSet {{ name: {:?}.to_owned(), entries: vec![{entries}] }}",
set.name
)
})
.collect::<Vec<_>>()
.join(",")
}
format!(
"#![allow(clippy::missing_errors_doc, clippy::must_use_candidate, clippy::should_implement_trait)]\n\n\
use std::sync::OnceLock;\n\n\
#[derive(Clone, Debug, Default, PartialEq)] pub struct AttentionData {{ priority: f32, timeout: f32 }}\n\
impl AttentionData {{ pub fn priority(&self) -> f32 {{ self.priority }} pub fn set_priority(&mut self, value: f32) {{ self.priority = value; }} pub fn timeout(&self) -> f32 {{ self.timeout }} pub fn set_timeout(&mut self, value: f32) {{ self.timeout = value; }} }}\n\n\
#[derive(Clone, Debug, Default, PartialEq)] pub struct AttentionSet {{ name: String, entries: Vec<AttentionData> }}\n\
impl AttentionSet {{ pub fn get(&self, type_: crate::LookAtType) -> Result<AttentionData, crate::Error> {{ self.entries.get(type_ as usize).cloned().ok_or(crate::Error::IndexOutOfRange) }} pub fn entries(&self) -> Vec<AttentionData> {{ self.entries.clone() }} pub fn set_entries(&mut self, value: Vec<AttentionData>) {{ self.entries = value; }} pub fn name(&self) -> String {{ self.name.clone() }} pub fn set_name(&mut self, value: String) {{ self.name = value; }} }}\n\n\
fn default_sets() -> &'static [AttentionSet] {{ static SETS: OnceLock<Vec<AttentionSet>> = OnceLock::new(); SETS.get_or_init(|| vec![{}]).as_slice() }}\n\
fn updated_sets() -> &'static [AttentionSet] {{ static SETS: OnceLock<Vec<AttentionSet>> = OnceLock::new(); SETS.get_or_init(|| vec![{}]).as_slice() }}\n\n\
pub struct LindenAttentions; impl LindenAttentions {{ pub fn default() -> Vec<AttentionSet> {{ default_sets().to_vec() }} pub fn updated() -> Vec<AttentionSet> {{ updated_sets().to_vec() }} pub fn default_masculine() -> AttentionSet {{ default_sets()[0].clone() }} pub fn default_feminine() -> AttentionSet {{ default_sets()[1].clone() }} pub fn updated_masculine() -> AttentionSet {{ updated_sets()[0].clone() }} pub fn updated_feminine() -> AttentionSet {{ updated_sets()[1].clone() }} }}\n\n\
#[cfg(test)] mod tests {{ use super::*; #[test] fn generated_attention_shape() {{ assert_eq!(default_sets().len(), 2); assert_eq!(updated_sets().len(), 2); assert!(default_sets().iter().chain(updated_sets()).all(|set| set.entries.len() == 11)); assert_eq!(default_sets()[0].name, \"Masculine\"); assert_eq!(default_sets()[1].name, \"Feminine\"); }} }}\n",
sets_expression(default),
sets_expression(updated)
)
}
fn render_genepool(archetypes: &[ArchetypeRecord]) -> String {
let values = archetypes
.iter()
.map(|archetype| {
let params = archetype
.params
.iter()
.map(|param| {
format!(
"ArchetypeParam {{ id: {}, name: {:?}.to_owned(), value: {} }}",
param.id,
param.name,
rust_f32(param.value)
)
})
.collect::<Vec<_>>()
.join(",");
format!(
"GenepoolArchetype {{ name: {:?}.to_owned(), params: vec![{params}] }}",
archetype.name
)
})
.collect::<Vec<_>>()
.join(",");
format!(
"#![allow(clippy::missing_errors_doc, clippy::must_use_candidate, clippy::needless_pass_by_value, clippy::too_many_lines)]\n\n\
use std::sync::OnceLock;\n\n\
#[derive(Clone, Debug, Default, PartialEq)] pub struct ArchetypeParam {{ id: i32, name: String, value: f32 }}\n\
impl ArchetypeParam {{ pub fn id(&self) -> i32 {{ self.id }} pub fn set_id(&mut self, value: i32) {{ self.id = value; }} pub fn name(&self) -> String {{ self.name.clone() }} pub fn set_name(&mut self, value: String) {{ self.name = value; }} pub fn value(&self) -> f32 {{ self.value }} pub fn set_value(&mut self, value: f32) {{ self.value = value; }} }}\n\n\
#[derive(Clone, Debug, Default, PartialEq)] pub struct GenepoolArchetype {{ name: String, params: Vec<ArchetypeParam> }}\n\
impl GenepoolArchetype {{ pub fn name(&self) -> String {{ self.name.clone() }} pub fn set_name(&mut self, value: String) {{ self.name = value; }} pub fn params(&self) -> Vec<ArchetypeParam> {{ self.params.clone() }} pub fn set_params(&mut self, value: Vec<ArchetypeParam>) {{ self.params = value; }} }}\n\n\
fn generated_archetypes() -> &'static [GenepoolArchetype] {{ static ARCHETYPES: OnceLock<Vec<GenepoolArchetype>> = OnceLock::new(); ARCHETYPES.get_or_init(|| vec![{values}]).as_slice() }}\n\n\
pub struct Genepool; impl Genepool {{ pub fn archetypes() -> Vec<GenepoolArchetype> {{ generated_archetypes().to_vec() }} pub fn find_index(name: String) -> Result<i32, crate::Error> {{ let Some(index) = generated_archetypes().iter().position(|archetype| archetype.name == name) else {{ return Ok(-1); }}; i32::try_from(index).map_err(|_| crate::Error::IndexOutOfRange) }} pub fn get(index: i32) -> Result<GenepoolArchetype, crate::Error> {{ usize::try_from(index).ok().and_then(|index| generated_archetypes().get(index)).cloned().ok_or(crate::Error::IndexOutOfRange) }} }}\n\n\
#[cfg(test)] mod tests {{ use super::*; #[test] fn generated_genepool_shape() {{ let archetypes = generated_archetypes(); assert_eq!(archetypes.len(), 24); assert_eq!(archetypes.iter().map(|archetype| archetype.params.len()).sum::<usize>(), 3360); assert_eq!(archetypes[0].name, \"M W Skinny\"); }} }}\n"
)
}
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 skeleton_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
let inventory = super::load_inventory(root)?;
verify_inputs(root, &inventory)?;
let input = load_input(&inventory, "avatar_skeleton")?;
let skeleton = parse_skeleton(&input.vendored_path, &load_text(root, input)?)?;
let body = format_body(&render_skeleton(&skeleton), "skeleton")?;
Ok(generated_rust("skeleton", &[input], &body))
}
pub(super) fn attention_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
let inventory = super::load_inventory(root)?;
verify_inputs(root, &inventory)?;
let default_input = load_input(&inventory, "attentions")?;
let updated_input = load_input(&inventory, "attentions_updated")?;
let default = parse_attentions(
&default_input.vendored_path,
&load_text(root, default_input)?,
)?;
let updated = parse_attentions(
&updated_input.vendored_path,
&load_text(root, updated_input)?,
)?;
let body = format_body(&render_attentions(&default, &updated), "attention")?;
Ok(generated_rust(
"attentions",
&[default_input, updated_input],
&body,
))
}
pub(super) fn genepool_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
let inventory = super::load_inventory(root)?;
verify_inputs(root, &inventory)?;
let input = load_input(&inventory, "genepool")?;
let archetypes = parse_genepool(&input.vendored_path, &load_text(root, input)?)?;
let body = format_body(&render_genepool(&archetypes), "genepool")?;
Ok(generated_rust("genepool", &[input], &body))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skeleton_schema_rejects_bad_counts_vectors_and_duplicate_names() {
let bad_count = "<linden_skeleton version=\"1\" num_bones=\"2\" num_collision_volumes=\"0\"><bone name=\"root\"/></linden_skeleton>";
assert!(
parse_skeleton("bad.xml", bad_count)
.unwrap_err()
.contains("error[SK006]")
);
let bad_vector = "<linden_skeleton version=\"1\" num_bones=\"1\" num_collision_volumes=\"0\"><bone name=\"root\" pos=\"1 2\"/></linden_skeleton>";
assert!(
parse_skeleton("bad.xml", bad_vector)
.unwrap_err()
.contains("error[SK011]")
);
let duplicate = "<linden_skeleton version=\"1\" num_bones=\"2\" num_collision_volumes=\"0\"><bone name=\"root\"><bone name=\"root\"/></bone></linden_skeleton>";
assert!(
parse_skeleton("bad.xml", duplicate)
.unwrap_err()
.contains("error[SK016]")
);
}
#[test]
fn attention_schema_rejects_unknown_and_incomplete_sets() {
let unknown = "<linden_attentions version=\"1\"><gender name=\"Masculine\"><param attention=\"unknown\" priority=\"1\" timeout=\"1\"/></gender><gender name=\"Feminine\"/></linden_attentions>";
assert!(
parse_attentions("bad.xml", unknown)
.unwrap_err()
.contains("error[AT009]")
);
let incomplete = "<linden_attentions version=\"1\"><gender name=\"Masculine\"/><gender name=\"Feminine\"/></linden_attentions>";
assert!(
parse_attentions("bad.xml", incomplete)
.unwrap_err()
.contains("error[AT013]")
);
}
#[test]
fn genepool_schema_rejects_bad_values_and_duplicate_ids() {
let bad_value = "<linden_genepool version=\"1\"><archetype name=\"A\"><param id=\"1\" name=\"P\" value=\"NaN\"/></archetype></linden_genepool>";
assert!(
parse_genepool("bad.xml", bad_value)
.unwrap_err()
.contains("error[GP010]")
);
let duplicate = "<linden_genepool version=\"1\"><archetype name=\"A\"><param id=\"1\" name=\"P\" value=\"0\"/><param id=\"1\" name=\"Q\" value=\"1\"/></archetype></linden_genepool>";
assert!(
parse_genepool("bad.xml", duplicate)
.unwrap_err()
.contains("error[GP008]")
);
}
#[test]
fn pinned_avatar_catalog_semantics_have_stable_digests() {
let root = super::super::workspace_root();
let inventory = super::super::load_inventory(&root).expect("pinned inventory");
let skeleton_input = load_input(&inventory, "avatar_skeleton").expect("skeleton input");
let skeleton = parse_skeleton(
&skeleton_input.vendored_path,
&load_text(&root, skeleton_input).expect("skeleton text"),
)
.expect("skeleton");
let attentions_input = load_input(&inventory, "attentions").expect("attentions input");
let updated_input =
load_input(&inventory, "attentions_updated").expect("updated attentions input");
let default = parse_attentions(
&attentions_input.vendored_path,
&load_text(&root, attentions_input).expect("attentions text"),
)
.expect("attentions");
let updated = parse_attentions(
&updated_input.vendored_path,
&load_text(&root, updated_input).expect("updated attentions text"),
)
.expect("updated attentions");
let genepool_input = load_input(&inventory, "genepool").expect("genepool input");
let genepool = parse_genepool(
&genepool_input.vendored_path,
&load_text(&root, genepool_input).expect("genepool text"),
)
.expect("genepool");
assert_eq!(
super::super::sha256(format!("{skeleton:#?}").as_bytes()),
"912dc35e06ac8f2eed48819f340ea18ab6173437cc251cda52081bc8445db4d6"
);
assert_eq!(
super::super::sha256(format!("{default:#?}\n{updated:#?}").as_bytes()),
"a0343e5d473ce2bfa29985e5d98afb066bf282b8786ba099aa76457b0b3d8af8"
);
assert_eq!(
super::super::sha256(format!("{genepool:#?}").as_bytes()),
"5e33fecab1a9501113096ee5ec38f52eadb7deb3de05a7c50c42e00c15c24a5b"
);
}
}

View File

@@ -11,6 +11,7 @@ use std::fmt::Write as _;
use std::fs;
use std::path::{Component, Path, PathBuf};
mod avatar_catalog;
mod xml_catalog;
pub const INVENTORY_PATH: &str = "codegen/sources.json";
@@ -18,6 +19,9 @@ pub const MANIFEST_OUTPUT: &str = "codegen/generated/source_manifest.rs";
pub const PACKET_OUTPUT: &str = "crates/libremetaverse/src/packet_catalog.rs";
pub const VISUAL_OUTPUT: &str = "crates/libremetaverse/src/visual_catalog.rs";
pub const FOLIAGE_OUTPUT: &str = "crates/libremetaverse/src/foliage_catalog.rs";
pub const SKELETON_OUTPUT: &str = "crates/libremetaverse/src/skeleton_catalog.rs";
pub const ATTENTION_OUTPUT: &str = "crates/libremetaverse/src/attention_catalog.rs";
pub const GENEPOOL_OUTPUT: &str = "crates/libremetaverse/src/genepool_catalog.rs";
pub const PUBLIC_API_PATH: &str = "api/public-api.json";
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
@@ -2055,6 +2059,18 @@ pub fn regenerate(root: &Path, check: bool) -> Result<(), String> {
root.join(FOLIAGE_OUTPUT),
xml_catalog::foliage_catalog_bytes(root)?,
),
(
root.join(SKELETON_OUTPUT),
avatar_catalog::skeleton_catalog_bytes(root)?,
),
(
root.join(ATTENTION_OUTPUT),
avatar_catalog::attention_catalog_bytes(root)?,
),
(
root.join(GENEPOOL_OUTPUT),
avatar_catalog::genepool_catalog_bytes(root)?,
),
];
if check {
for (path, expected) in outputs {
@@ -2099,6 +2115,21 @@ mod tests {
let second_foliage =
xml_catalog::foliage_catalog_bytes(&root).expect("second foliage generation");
assert_eq!(first_foliage, second_foliage);
let first_skeleton =
avatar_catalog::skeleton_catalog_bytes(&root).expect("first skeleton generation");
let second_skeleton =
avatar_catalog::skeleton_catalog_bytes(&root).expect("second skeleton generation");
assert_eq!(first_skeleton, second_skeleton);
let first_attention =
avatar_catalog::attention_catalog_bytes(&root).expect("first attention generation");
let second_attention =
avatar_catalog::attention_catalog_bytes(&root).expect("second attention generation");
assert_eq!(first_attention, second_attention);
let first_genepool =
avatar_catalog::genepool_catalog_bytes(&root).expect("first genepool generation");
let second_genepool =
avatar_catalog::genepool_catalog_bytes(&root).expect("second genepool generation");
assert_eq!(first_genepool, second_genepool);
}
#[test]