Skip to main content

yog_api/
entities.rs

1//! Mod entities the loader draws: a type registered with a `model` gets its
2//! body model, playing the clip of its state — `walk` while it walks, else
3//! `idle`, or the state in its entity data `yog:state` (a string the mod
4//! sets on the server) — timed by the world's clock, alike for everyone.
5//!
6//! A hanging entity (`kind: hanging`) tells the mods how it hangs: its
7//! entity data [`HANGING`], read with [`Hanging`].
8
9use 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
16/// The entity data keys: a mod's entity's state ([`STATE`]) and a hanging
17/// entity's [`Hanging`] ([`HANGING`], set by the loader on the server and for
18/// every client that sees it).
19pub use yog_ids::yog::{HANGING, STATE};
20
21/// How a hanging entity hangs: on which side, how big, and the item it was
22/// placed from (what it drops).
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Hanging {
25    /// The way it faces, out of its wall: `north`, `south`, `west`, `east`.
26    pub facing: String,
27    /// Whole blocks of wall, wide and high.
28    pub width: u32,
29    pub height: u32,
30    /// The item it keeps: its id and custom data (SNBT, `{}` for none).
31    pub item: String,
32    pub nbt: String,
33}
34
35impl Hanging {
36    /// Out of its entity data ([`HANGING`]): little-endian, `facing` and
37    /// `item` a u16 length and UTF-8, `width`, `height` u16, `nbt` a u32
38    /// length and UTF-8.
39    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    /// The hanging entity a body frame is about, if it is one.
62    pub fn of(frame: &Frame) -> Option<Hanging> {
63        Hanging::decode(frame.data(HANGING)?)
64    }
65
66    /// Hanging entity `uuid` on the server, if it is one.
67    pub fn get(srv: &dyn yog_core::Server, uuid: &str) -> Option<Hanging> {
68        Hanging::decode(&srv.entity_data(uuid, HANGING)?)
69    }
70}
71
72/// Above this limb swing an entity walks.
73const 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
84/// Remembers how `def` looks; `true` for the first such type (the caller
85/// hooks [`draw`] then).
86pub(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}