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 key a mod's entity's state goes under.
17pub const STATE: &str = "yog:state";
18
19/// The entity data key a hanging entity's [`Hanging`] goes under — the
20/// loader sets it, on the server and for every client that sees it.
21pub const HANGING: &str = "yog:hanging";
22
23/// How a hanging entity hangs: on which side, how big, and the item it was
24/// placed from (what it drops).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Hanging {
27    /// The way it faces, out of its wall: `north`, `south`, `west`, `east`.
28    pub facing: String,
29    /// Whole blocks of wall, wide and high.
30    pub width: u32,
31    pub height: u32,
32    /// The item it keeps: its id and custom data (SNBT, `{}` for none).
33    pub item: String,
34    pub nbt: String,
35}
36
37impl Hanging {
38    /// Out of its entity data ([`HANGING`]): little-endian, `facing` and
39    /// `item` a u16 length and UTF-8, `width`, `height` u16, `nbt` a u32
40    /// length and UTF-8.
41    pub fn decode(mut b: &[u8]) -> Option<Hanging> {
42        fn take<'a>(b: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
43            let (h, t) = (b.get(..n)?, b.get(n..)?);
44            *b = t;
45            Some(h)
46        }
47        fn u16(b: &mut &[u8]) -> Option<u16> {
48            take(b, 2).map(|s| u16::from_le_bytes([s[0], s[1]]))
49        }
50        fn str(b: &mut &[u8], n: usize) -> Option<String> {
51            String::from_utf8(take(b, n)?.to_vec()).ok()
52        }
53        let n = u16(&mut b)? as usize;
54        let facing = str(&mut b, n)?;
55        let (width, height) = (u16(&mut b)? as u32, u16(&mut b)? as u32);
56        let n = u16(&mut b)? as usize;
57        let item = str(&mut b, n)?;
58        let n = take(&mut b, 4).map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))? as usize;
59        let nbt = str(&mut b, n)?;
60        Some(Hanging { facing, width, height, item, nbt })
61    }
62
63    /// The hanging entity a body frame is about, if it is one.
64    pub fn of(frame: &Frame) -> Option<Hanging> {
65        Hanging::decode(frame.data(HANGING)?)
66    }
67
68    /// Hanging entity `uuid` on the server, if it is one.
69    pub fn get(srv: &dyn yog_core::Server, uuid: &str) -> Option<Hanging> {
70        Hanging::decode(&srv.entity_data(uuid, HANGING)?)
71    }
72}
73
74/// Above this limb swing an entity walks.
75const WALKING: f32 = 0.01;
76
77struct Look {
78    model: String,
79    animations: Option<String>,
80    clips: Vec<(String, String)>,
81}
82
83static LOOKS: LazyLock<Mutex<HashMap<String, Look>>> = LazyLock::new(Default::default);
84static HOOKED: AtomicBool = AtomicBool::new(false);
85
86fn qualify(id: &str, of: &str) -> String {
87    if id.contains(':') {
88        id.to_owned()
89    } else {
90        let ns = of.split_once(':').map_or("minecraft", |(n, _)| n);
91        format!("{ns}:{id}")
92    }
93}
94
95/// Remembers how `def` looks; `true` for the first such type (the caller
96/// hooks [`draw`] then).
97pub(crate) fn drawn(def: &EntityDef) -> bool {
98    let Some(model) = &def.model else { return false };
99    LOOKS.lock().unwrap_or_else(|e| e.into_inner()).insert(
100        def.id.clone(),
101        Look {
102            model: qualify(model, &def.id),
103            animations: def.animations.as_ref().map(|a| qualify(a, &def.id)),
104            clips: def.clips.0.clone(),
105        },
106    );
107    crate::body::track_type(&def.id, true);
108    !HOOKED.swap(true, Ordering::AcqRel)
109}
110
111pub(crate) fn draw(f: &Frame, ops: &mut Ops) {
112    let looks = LOOKS.lock().unwrap_or_else(|e| e.into_inner());
113    let Some(look) = looks.get(&f.entity_type) else { return };
114    let Some(model) = yog_body::model(&look.model) else { return };
115    let state = f
116        .data(STATE)
117        .and_then(|s| std::str::from_utf8(s).ok())
118        .map(str::to_owned)
119        .unwrap_or_else(|| if f.limb_amount > WALKING { "walk" } else { "idle" }.to_owned());
120    let mut rig = Rig::new(model, Anchor::Model);
121    let clip = look.clips.iter().find(|(s, _)| *s == state).map(|(_, c)| c.as_str()).unwrap_or(state.as_str());
122    if let Some(anims) = &look.animations
123        && let Some(c) = yog_body::clip(anims, clip)
124    {
125        rig.play(c, 0.0);
126    }
127    rig.write(ops, f.world_seconds());
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn a_hanging_entity_reads_as_the_host_writes_it() {
136        let mut b = Vec::new();
137        b.extend_from_slice(&5u16.to_le_bytes());
138        b.extend_from_slice(b"north");
139        b.extend_from_slice(&2u16.to_le_bytes());
140        b.extend_from_slice(&3u16.to_le_bytes());
141        b.extend_from_slice(&10u16.to_le_bytes());
142        b.extend_from_slice(b"m:painting");
143        b.extend_from_slice(&5u32.to_le_bytes());
144        b.extend_from_slice(b"{a:1}");
145        let h = Hanging::decode(&b).unwrap();
146        assert_eq!((h.facing.as_str(), h.width, h.height), ("north", 2, 3));
147        assert_eq!((h.item.as_str(), h.nbt.as_str()), ("m:painting", "{a:1}"));
148        assert!(Hanging::decode(&b[..b.len() - 1]).is_none(), "cut short: nothing");
149    }
150}