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
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{LazyLock, Mutex};
9
10use yog_body::{Anchor, Frame, Ops, Rig};
11use yog_registry::EntityDef;
12
13/// The entity data key a mod's entity's state goes under.
14pub const STATE: &str = "yog:state";
15
16/// Above this limb swing an entity walks.
17const WALKING: f32 = 0.01;
18
19struct Look {
20    model: String,
21    animations: Option<String>,
22    clips: Vec<(String, String)>,
23}
24
25static LOOKS: LazyLock<Mutex<HashMap<String, Look>>> = LazyLock::new(Default::default);
26static HOOKED: AtomicBool = AtomicBool::new(false);
27
28fn qualify(id: &str, of: &str) -> String {
29    if id.contains(':') {
30        id.to_owned()
31    } else {
32        let ns = of.split_once(':').map_or("minecraft", |(n, _)| n);
33        format!("{ns}:{id}")
34    }
35}
36
37/// Remembers how `def` looks; `true` for the first such type (the caller
38/// hooks [`draw`] then).
39pub(crate) fn drawn(def: &EntityDef) -> bool {
40    let Some(model) = &def.model else { return false };
41    LOOKS.lock().unwrap_or_else(|e| e.into_inner()).insert(
42        def.id.clone(),
43        Look {
44            model: qualify(model, &def.id),
45            animations: def.animations.as_ref().map(|a| qualify(a, &def.id)),
46            clips: def.clips.0.clone(),
47        },
48    );
49    crate::body::track_type(&def.id, true);
50    !HOOKED.swap(true, Ordering::AcqRel)
51}
52
53pub(crate) fn draw(f: &Frame, ops: &mut Ops) {
54    let looks = LOOKS.lock().unwrap_or_else(|e| e.into_inner());
55    let Some(look) = looks.get(&f.entity_type) else { return };
56    let Some(model) = yog_body::model(&look.model) else { return };
57    let state = f
58        .data(STATE)
59        .and_then(|s| std::str::from_utf8(s).ok())
60        .map(str::to_owned)
61        .unwrap_or_else(|| if f.limb_amount > WALKING { "walk" } else { "idle" }.to_owned());
62    let mut rig = Rig::new(model, Anchor::Model);
63    let clip = look.clips.iter().find(|(s, _)| *s == state).map(|(_, c)| c.as_str()).unwrap_or(state.as_str());
64    if let Some(anims) = &look.animations
65        && let Some(c) = yog_body::clip(anims, clip)
66    {
67        rig.play(c, 0.0);
68    }
69    rig.write(ops, f.world_seconds());
70}