1use std::collections::HashMap;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{LazyLock, Mutex};
12
13use yog_body::{Anchor, Frame, Ops, Rig};
14use yog_registry::EntityDef;
15
16pub use yog_ids::yog::{HANGING, STATE};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Hanging {
25 pub facing: String,
27 pub width: u32,
29 pub height: u32,
30 pub item: String,
32 pub nbt: String,
33}
34
35impl Hanging {
36 pub fn decode(mut b: &[u8]) -> Option<Hanging> {
40 fn take<'a>(b: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
41 let (h, t) = (b.get(..n)?, b.get(n..)?);
42 *b = t;
43 Some(h)
44 }
45 fn u16(b: &mut &[u8]) -> Option<u16> {
46 take(b, 2).map(|s| u16::from_le_bytes([s[0], s[1]]))
47 }
48 fn str(b: &mut &[u8], n: usize) -> Option<String> {
49 String::from_utf8(take(b, n)?.to_vec()).ok()
50 }
51 let n = u16(&mut b)? as usize;
52 let facing = str(&mut b, n)?;
53 let (width, height) = (u16(&mut b)? as u32, u16(&mut b)? as u32);
54 let n = u16(&mut b)? as usize;
55 let item = str(&mut b, n)?;
56 let n = take(&mut b, 4).map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))? as usize;
57 let nbt = str(&mut b, n)?;
58 Some(Hanging { facing, width, height, item, nbt })
59 }
60
61 pub fn of(frame: &Frame) -> Option<Hanging> {
63 Hanging::decode(frame.data(HANGING)?)
64 }
65
66 pub fn get(srv: &dyn yog_core::Server, uuid: &str) -> Option<Hanging> {
68 Hanging::decode(&srv.entity_data(uuid, HANGING)?)
69 }
70}
71
72const WALKING: f32 = 0.01;
74
75struct Look {
76 model: String,
77 animations: Option<String>,
78 clips: Vec<(String, String)>,
79}
80
81static LOOKS: LazyLock<Mutex<HashMap<String, Look>>> = LazyLock::new(Default::default);
82static HOOKED: AtomicBool = AtomicBool::new(false);
83
84pub(crate) fn drawn(def: &EntityDef) -> bool {
87 let Some(model) = &def.model else { return false };
88 LOOKS.lock().unwrap_or_else(|e| e.into_inner()).insert(
89 def.id.clone(),
90 Look {
91 model: yog_ids::qualify_in(yog_ids::split(&def.id).0, model),
92 animations: def.animations.as_ref().map(|a| yog_ids::qualify_in(yog_ids::split(&def.id).0, a)),
93 clips: def.clips.0.clone(),
94 },
95 );
96 crate::body::track_type(&def.id, true);
97 !HOOKED.swap(true, Ordering::AcqRel)
98}
99
100pub(crate) fn draw(f: &Frame, ops: &mut Ops) {
101 let looks = LOOKS.lock().unwrap_or_else(|e| e.into_inner());
102 let Some(look) = looks.get(&f.entity_type) else { return };
103 let Some(model) = yog_body::model(&look.model) else { return };
104 let state = f
105 .data(STATE)
106 .and_then(|s| std::str::from_utf8(s).ok())
107 .map(str::to_owned)
108 .unwrap_or_else(|| if f.limb_amount > WALKING { "walk" } else { "idle" }.to_owned());
109 let mut rig = Rig::new(model, Anchor::Model);
110 let clip = look.clips.iter().find(|(s, _)| *s == state).map(|(_, c)| c.as_str()).unwrap_or(state.as_str());
111 if let Some(anims) = &look.animations
112 && let Some(c) = yog_body::clip(anims, clip)
113 {
114 rig.play(c, 0.0);
115 }
116 rig.write(ops, f.world_seconds());
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn a_hanging_entity_reads_as_the_host_writes_it() {
125 let mut b = Vec::new();
126 b.extend_from_slice(&5u16.to_le_bytes());
127 b.extend_from_slice(b"north");
128 b.extend_from_slice(&2u16.to_le_bytes());
129 b.extend_from_slice(&3u16.to_le_bytes());
130 b.extend_from_slice(&10u16.to_le_bytes());
131 b.extend_from_slice(b"m:painting");
132 b.extend_from_slice(&5u32.to_le_bytes());
133 b.extend_from_slice(b"{a:1}");
134 let h = Hanging::decode(&b).unwrap();
135 assert_eq!((h.facing.as_str(), h.width, h.height), ("north", 2, 3));
136 assert_eq!((h.item.as_str(), h.nbt.as_str()), ("m:painting", "{a:1}"));
137 assert!(Hanging::decode(&b[..b.len() - 1]).is_none(), "cut short: nothing");
138 }
139}