Generate skeleton attention and genepool catalogs (#49)
This commit is contained in:
881
tools/codegen/src/avatar_catalog.rs
Normal file
881
tools/codegen/src/avatar_catalog.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user