1pub mod manifest;
15pub mod races;
16
17use std::collections::HashMap;
18
19use thread_id::StructuredId;
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum AvatarSlot {
28 Body,
30 SkinType,
31 SkinTone,
32 Eyes,
33 Ears,
34 Tail,
35 Claws,
36 Gills,
37 Webbing,
38 Horns,
39 Wings,
40 Snout,
41 Coat,
42 Hair,
44 HairColor,
45 Shirt,
47 Pants,
48 Shoes,
49 Jacket,
50 Headgear,
51 Socks,
52 Gloves,
53 AccessoryHead,
55 AccessoryBody,
56 AccessoryLapel,
57 Offhand,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum SlotCategory {
63 Biology,
64 Hair,
65 Clothing,
66 Accessory,
67}
68
69impl AvatarSlot {
70 pub const ALL: [AvatarSlot; 26] = [
72 AvatarSlot::Body,
73 AvatarSlot::SkinType,
74 AvatarSlot::SkinTone,
75 AvatarSlot::Eyes,
76 AvatarSlot::Ears,
77 AvatarSlot::Tail,
78 AvatarSlot::Claws,
79 AvatarSlot::Gills,
80 AvatarSlot::Webbing,
81 AvatarSlot::Horns,
82 AvatarSlot::Wings,
83 AvatarSlot::Snout,
84 AvatarSlot::Coat,
85 AvatarSlot::Hair,
86 AvatarSlot::HairColor,
87 AvatarSlot::Shirt,
88 AvatarSlot::Pants,
89 AvatarSlot::Shoes,
90 AvatarSlot::Jacket,
91 AvatarSlot::Headgear,
92 AvatarSlot::Socks,
93 AvatarSlot::Gloves,
94 AvatarSlot::AccessoryHead,
95 AvatarSlot::AccessoryBody,
96 AvatarSlot::AccessoryLapel,
97 AvatarSlot::Offhand,
98 ];
99
100 pub fn category(self) -> SlotCategory {
102 use AvatarSlot::*;
103 match self {
104 Body | SkinType | SkinTone | Eyes | Ears | Tail | Claws | Gills | Webbing | Horns
105 | Wings | Snout | Coat => SlotCategory::Biology,
106 Hair | HairColor => SlotCategory::Hair,
107 Shirt | Pants | Shoes | Jacket | Headgear | Socks | Gloves => SlotCategory::Clothing,
108 AccessoryHead | AccessoryBody | AccessoryLapel | Offhand => SlotCategory::Accessory,
109 }
110 }
111
112 pub fn name(self) -> &'static str {
115 match self {
116 AvatarSlot::Body => "Body",
117 AvatarSlot::SkinType => "SkinType",
118 AvatarSlot::SkinTone => "SkinTone",
119 AvatarSlot::Eyes => "Eyes",
120 AvatarSlot::Ears => "Ears",
121 AvatarSlot::Tail => "Tail",
122 AvatarSlot::Claws => "Claws",
123 AvatarSlot::Gills => "Gills",
124 AvatarSlot::Webbing => "Webbing",
125 AvatarSlot::Horns => "Horns",
126 AvatarSlot::Wings => "Wings",
127 AvatarSlot::Snout => "Snout",
128 AvatarSlot::Coat => "Coat",
129 AvatarSlot::Hair => "Hair",
130 AvatarSlot::HairColor => "HairColor",
131 AvatarSlot::Shirt => "Shirt",
132 AvatarSlot::Pants => "Pants",
133 AvatarSlot::Shoes => "Shoes",
134 AvatarSlot::Jacket => "Jacket",
135 AvatarSlot::Headgear => "Headgear",
136 AvatarSlot::Socks => "Socks",
137 AvatarSlot::Gloves => "Gloves",
138 AvatarSlot::AccessoryHead => "AccessoryHead",
139 AvatarSlot::AccessoryBody => "AccessoryBody",
140 AvatarSlot::AccessoryLapel => "AccessoryLapel",
141 AvatarSlot::Offhand => "Offhand",
142 }
143 }
144
145 pub fn from_name(name: &str) -> Option<Self> {
147 Self::ALL.into_iter().find(|s| s.name() == name)
148 }
149
150 pub fn is_biology(self) -> bool {
152 self.category() == SlotCategory::Biology
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AvatarSpec {
173 #[serde(default)]
174 parts: HashMap<AvatarSlot, u32>,
175 #[serde(rename = "bodyHeight", default = "half")]
177 pub body_height: f32,
178}
179
180fn half() -> f32 {
181 0.5
182}
183
184impl Default for AvatarSpec {
185 fn default() -> Self {
186 Self {
187 parts: HashMap::new(),
188 body_height: 0.5,
189 }
190 }
191}
192
193impl AvatarSpec {
194 pub fn new() -> Self {
195 Self::default()
196 }
197
198 pub fn get(&self, slot: AvatarSlot) -> u32 {
200 self.parts.get(&slot).copied().unwrap_or(0)
201 }
202
203 pub fn set(&mut self, slot: AvatarSlot, part_id: u32) {
205 if part_id == 0 {
206 self.parts.remove(&slot);
207 } else {
208 self.parts.insert(slot, part_id);
209 }
210 }
211
212 pub fn has(&self, slot: AvatarSlot) -> bool {
213 self.parts.contains_key(&slot)
214 }
215
216 pub fn clear(&mut self, slot: AvatarSlot) {
217 self.parts.remove(&slot);
218 }
219
220 pub fn iter(&self) -> impl Iterator<Item = (AvatarSlot, u32)> + '_ {
222 self.parts.iter().map(|(&s, &id)| (s, id))
223 }
224
225 pub fn from_config(config: &HashMap<String, u32>) -> Self {
229 let mut spec = Self::default();
230 for (name, part_id) in config {
231 if let Some(slot) = AvatarSlot::from_name(name) {
232 spec.set(slot, *part_id);
233 }
234 }
235 spec
236 }
237
238 pub fn to_config(&self) -> HashMap<String, u32> {
240 self.parts
241 .iter()
242 .map(|(slot, id)| (slot.name().to_string(), *id))
243 .collect()
244 }
245
246 pub fn filled_count(&self) -> usize {
247 self.parts.len()
248 }
249}
250
251#[derive(Debug, Clone)]
257pub struct AvatarPart {
258 pub id: StructuredId,
259 pub slot: AvatarSlot,
260 pub display_name: String,
261 pub tint: [f32; 4],
263 pub owned_by_default: bool,
265 pub mesh: Option<String>,
267 pub sprite: Option<String>,
269 pub sorting_order: i32,
271}
272
273impl AvatarPart {
274 pub fn mesh_part(id: StructuredId, slot: AvatarSlot, name: &str, mesh: &str) -> Self {
276 Self {
277 id,
278 slot,
279 display_name: name.to_string(),
280 tint: [1.0, 1.0, 1.0, 1.0],
281 owned_by_default: true,
282 mesh: Some(mesh.to_string()),
283 sprite: None,
284 sorting_order: 0,
285 }
286 }
287}
288
289#[derive(Debug, Clone)]
292pub struct AvatarRaceMode {
293 pub id: StructuredId,
294 pub name: String,
295 pub slot_defaults: HashMap<AvatarSlot, u32>,
297 pub morph_weight: f32,
299}
300
301impl AvatarRaceMode {
302 pub fn new(id: StructuredId, name: &str) -> Self {
303 Self {
304 id,
305 name: name.to_string(),
306 slot_defaults: HashMap::new(),
307 morph_weight: 1.0,
308 }
309 }
310
311 pub fn with_default(mut self, slot: AvatarSlot, part_id: u32) -> Self {
312 self.slot_defaults.insert(slot, part_id);
313 self
314 }
315
316 pub fn apply_to(&self, spec: &mut AvatarSpec) {
318 for (&slot, &id) in &self.slot_defaults {
319 spec.set(slot, id);
320 }
321 }
322}
323
324#[derive(Debug, Clone, Default)]
326pub struct AvatarCatalog {
327 parts: Vec<AvatarPart>,
328}
329
330impl AvatarCatalog {
331 pub fn new() -> Self {
332 Self::default()
333 }
334
335 pub fn add(&mut self, part: AvatarPart) {
336 self.parts.push(part);
337 }
338
339 pub fn get(&self, id: StructuredId) -> Option<&AvatarPart> {
341 self.parts.iter().find(|p| p.id == id)
342 }
343
344 pub fn parts_for_slot(&self, slot: AvatarSlot) -> impl Iterator<Item = &AvatarPart> {
346 self.parts.iter().filter(move |p| p.slot == slot)
347 }
348
349 pub fn len(&self) -> usize {
350 self.parts.len()
351 }
352
353 pub fn is_empty(&self) -> bool {
354 self.parts.is_empty()
355 }
356}
357
358pub trait AvatarRenderer {
361 fn clear(&mut self);
363 fn set_body(&mut self, body: &AvatarPart, body_height: f32);
365 fn set_part(&mut self, slot: AvatarSlot, part: &AvatarPart);
367 fn commit(&mut self);
369}
370
371pub struct AvatarBuilder<'a> {
373 catalog: &'a AvatarCatalog,
374}
375
376impl<'a> AvatarBuilder<'a> {
377 pub fn new(catalog: &'a AvatarCatalog) -> Self {
378 Self { catalog }
379 }
380
381 pub fn build(&self, spec: &AvatarSpec, renderer: &mut impl AvatarRenderer) {
385 renderer.clear();
386
387 let body_id = spec.get(AvatarSlot::Body);
389 if body_id != 0 {
390 if let Some(body) = self.catalog.get(StructuredId(body_id)) {
391 renderer.set_body(body, spec.body_height);
392 }
393 }
394
395 for &slot in AvatarSlot::ALL.iter() {
396 if slot == AvatarSlot::Body {
397 continue;
398 }
399 let id = spec.get(slot);
400 if id == 0 {
401 continue;
402 }
403 if let Some(part) = self.catalog.get(StructuredId(id)) {
404 renderer.set_part(slot, part);
405 }
406 }
407
408 renderer.commit();
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn part(id: u32, slot: AvatarSlot, name: &str) -> AvatarPart {
417 AvatarPart::mesh_part(StructuredId(id), slot, name, "mesh_key")
418 }
419
420 #[test]
424 fn slot_wire_names_match_the_unity_enum() {
425 let expected = [
426 "Body",
427 "SkinType",
428 "SkinTone",
429 "Eyes",
430 "Ears",
431 "Tail",
432 "Claws",
433 "Gills",
434 "Webbing",
435 "Horns",
436 "Wings",
437 "Snout",
438 "Coat",
439 "Hair",
440 "HairColor",
441 "Shirt",
442 "Pants",
443 "Shoes",
444 "Jacket",
445 "Headgear",
446 "Socks",
447 "Gloves",
448 "AccessoryHead",
449 "AccessoryBody",
450 "AccessoryLapel",
451 "Offhand",
452 ];
453 assert_eq!(AvatarSlot::ALL.len(), expected.len());
454 for (slot, want) in AvatarSlot::ALL.into_iter().zip(expected) {
455 assert_eq!(slot.name(), want);
456 assert_eq!(AvatarSlot::from_name(want), Some(slot));
457 assert_eq!(serde_json::to_string(&slot).unwrap(), format!("\"{want}\""));
459 }
460 }
461
462 #[test]
464 fn spec_speaks_the_shared_wire_shape() {
465 let spec: AvatarSpec = serde_json::from_str(
466 r#"{ "bodyHeight": 0.7, "parts": { "Body": 11010001, "Jacket": 123456 } }"#,
467 )
468 .unwrap();
469 assert_eq!(spec.body_height, 0.7);
470 assert_eq!(spec.get(AvatarSlot::Body), 11010001);
471 assert_eq!(spec.get(AvatarSlot::Jacket), 123456);
472 let bare: AvatarSpec = serde_json::from_str("{}").unwrap();
474 assert_eq!(bare.body_height, 0.5);
475 assert_eq!(bare.filled_count(), 0);
476 let back: AvatarSpec =
478 serde_json::from_str(&serde_json::to_string(&spec).unwrap()).unwrap();
479 assert_eq!(back.get(AvatarSlot::Jacket), 123456);
480 assert_eq!(back.body_height, 0.7);
481 let config = spec.to_config();
483 assert_eq!(config["Jacket"], 123456);
484 let again = AvatarSpec::from_config(&config);
485 assert_eq!(again.get(AvatarSlot::Body), 11010001);
486 }
487
488 #[test]
489 fn slot_categories() {
490 assert_eq!(AvatarSlot::Tail.category(), SlotCategory::Biology);
491 assert!(AvatarSlot::Tail.is_biology());
492 assert_eq!(AvatarSlot::Hair.category(), SlotCategory::Hair);
493 assert_eq!(AvatarSlot::Shirt.category(), SlotCategory::Clothing);
494 assert_eq!(AvatarSlot::Offhand.category(), SlotCategory::Accessory);
495 assert!(!AvatarSlot::Shirt.is_biology());
496 assert_eq!(AvatarSlot::ALL.len(), 26);
498 }
499
500 #[test]
501 fn spec_get_set_clear() {
502 let mut spec = AvatarSpec::new();
503 assert_eq!(spec.body_height, 0.5);
504 assert_eq!(spec.get(AvatarSlot::Shirt), 0);
505 assert!(!spec.has(AvatarSlot::Shirt));
506
507 spec.set(AvatarSlot::Shirt, 42);
508 assert_eq!(spec.get(AvatarSlot::Shirt), 42);
509 assert!(spec.has(AvatarSlot::Shirt));
510 assert_eq!(spec.filled_count(), 1);
511
512 spec.set(AvatarSlot::Shirt, 0);
514 assert!(!spec.has(AvatarSlot::Shirt));
515 assert_eq!(spec.filled_count(), 0);
516 }
517
518 #[test]
519 fn race_applies_biology_defaults() {
520 let mut spec = AvatarSpec::new();
521 spec.set(AvatarSlot::Shirt, 100); let reptilian = AvatarRaceMode::new(StructuredId(1), "Reptilian")
524 .with_default(AvatarSlot::SkinType, 7)
525 .with_default(AvatarSlot::Tail, 8)
526 .with_default(AvatarSlot::Claws, 9);
527 reptilian.apply_to(&mut spec);
528
529 assert_eq!(spec.get(AvatarSlot::SkinType), 7);
530 assert_eq!(spec.get(AvatarSlot::Tail), 8);
531 assert_eq!(spec.get(AvatarSlot::Claws), 9);
532 assert_eq!(spec.get(AvatarSlot::Shirt), 100);
534 }
535
536 #[test]
537 fn catalog_lookup() {
538 let mut cat = AvatarCatalog::new();
539 cat.add(part(10, AvatarSlot::Hair, "Spiky"));
540 cat.add(part(11, AvatarSlot::Hair, "Bob"));
541 cat.add(part(20, AvatarSlot::Shirt, "Tunic"));
542
543 assert_eq!(cat.len(), 3);
544 assert_eq!(cat.get(StructuredId(11)).unwrap().display_name, "Bob");
545 assert!(cat.get(StructuredId(999)).is_none());
546 assert_eq!(cat.parts_for_slot(AvatarSlot::Hair).count(), 2);
547 assert_eq!(cat.parts_for_slot(AvatarSlot::Shirt).count(), 1);
548 }
549
550 #[derive(Default)]
552 struct RecordingRenderer {
553 log: Vec<String>,
554 }
555 impl AvatarRenderer for RecordingRenderer {
556 fn clear(&mut self) {
557 self.log.push("clear".into());
558 }
559 fn set_body(&mut self, body: &AvatarPart, h: f32) {
560 self.log.push(format!("body:{}:{}", body.display_name, h));
561 }
562 fn set_part(&mut self, slot: AvatarSlot, part: &AvatarPart) {
563 self.log
564 .push(format!("part:{:?}:{}", slot, part.display_name));
565 }
566 fn commit(&mut self) {
567 self.log.push("commit".into());
568 }
569 }
570
571 #[test]
572 fn builder_assembles_in_order() {
573 let mut cat = AvatarCatalog::new();
574 cat.add(part(1, AvatarSlot::Body, "HumanBody"));
575 cat.add(part(2, AvatarSlot::Hair, "Spiky"));
576 cat.add(part(3, AvatarSlot::Shirt, "Tunic"));
577
578 let mut spec = AvatarSpec::new();
579 spec.body_height = 0.7;
580 spec.set(AvatarSlot::Body, 1);
581 spec.set(AvatarSlot::Hair, 2);
582 spec.set(AvatarSlot::Shirt, 3);
583 spec.set(AvatarSlot::Pants, 999); let mut r = RecordingRenderer::default();
586 AvatarBuilder::new(&cat).build(&spec, &mut r);
587
588 assert_eq!(r.log[0], "clear");
589 assert_eq!(r.log[1], "body:HumanBody:0.7");
590 let hair_idx = r.log.iter().position(|s| s.contains("Spiky")).unwrap();
592 let shirt_idx = r.log.iter().position(|s| s.contains("Tunic")).unwrap();
593 assert!(hair_idx < shirt_idx);
594 assert!(!r.log.iter().any(|s| s.contains("Pants"))); assert_eq!(r.log.last().unwrap(), "commit");
596 }
597
598 #[test]
599 fn spec_serde_roundtrip() {
600 let mut spec = AvatarSpec::new();
601 spec.body_height = 0.9;
602 spec.set(AvatarSlot::Hair, 5);
603 spec.set(AvatarSlot::Tail, 8);
604
605 let json = serde_json::to_string(&spec).unwrap();
606 let back: AvatarSpec = serde_json::from_str(&json).unwrap();
607 assert_eq!(back.body_height, 0.9);
608 assert_eq!(back.get(AvatarSlot::Hair), 5);
609 assert_eq!(back.get(AvatarSlot::Tail), 8);
610 }
611}