nightshade 0.14.1

A cross-platform data-oriented game engine.
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::ecs::animation::components::{AnimationClip, AnimationPlayer};

use super::asset_uuid::AssetUuid;
use super::registry::{asset_registry_resolve_name, asset_registry_resolve_uuid};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneAnimationClipRef {
    #[serde(default)]
    pub uuid: Option<AssetUuid>,
    #[serde(default)]
    pub path: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
}

impl SceneAnimationClipRef {
    pub fn from_uuid(uuid: AssetUuid) -> Self {
        Self {
            uuid: Some(uuid),
            path: None,
            name: None,
        }
    }

    pub fn from_path(path: impl Into<String>) -> Self {
        Self {
            uuid: None,
            path: Some(path.into()),
            name: None,
        }
    }

    pub fn from_name(name: impl Into<String>) -> Self {
        Self {
            uuid: None,
            path: None,
            name: Some(name.into()),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneAnimationPlayer {
    #[serde(default)]
    pub clips: Vec<AnimationClip>,
    #[serde(default)]
    pub clip_refs: Vec<SceneAnimationClipRef>,
    #[serde(default)]
    pub current_clip: Option<usize>,
    #[serde(default)]
    pub time: f32,
    #[serde(default = "default_animation_speed")]
    pub speed: f32,
    #[serde(default = "default_true")]
    pub looping: bool,
    #[serde(default)]
    pub playing: bool,
    #[serde(default)]
    pub play_all: bool,
    /// Maps bone names from the original glTF skeleton to the AssetUuid
    /// of the entity that represents that bone in the scene. Resolved
    /// back to entities at load time so skinned animations target the
    /// correct entities.
    #[serde(default)]
    pub bone_name_to_node: HashMap<String, AssetUuid>,
    /// Maps glTF node indices (the target of animation channels that
    /// don't have a bone name, e.g. rigid object animations) to the
    /// AssetUuid of the entity representing that node. Without this,
    /// non-skinned animations would round-trip with empty target maps
    /// and channels would have no entity to apply to.
    #[serde(default)]
    pub node_index_to_node: HashMap<usize, AssetUuid>,
}

fn default_animation_speed() -> f32 {
    1.0
}

fn default_true() -> bool {
    true
}

impl Default for SceneAnimationPlayer {
    fn default() -> Self {
        Self {
            clips: Vec::new(),
            clip_refs: Vec::new(),
            current_clip: None,
            time: 0.0,
            speed: 1.0,
            looping: true,
            playing: false,
            play_all: false,
            bone_name_to_node: HashMap::new(),
            node_index_to_node: HashMap::new(),
        }
    }
}

impl SceneAnimationPlayer {
    pub fn new(clips: Vec<AnimationClip>) -> Self {
        Self {
            clips,
            ..Default::default()
        }
    }

    pub fn with_current_clip(mut self, clip_index: usize) -> Self {
        self.current_clip = Some(clip_index);
        self
    }

    pub fn with_playing(mut self, playing: bool) -> Self {
        self.playing = playing;
        self
    }

    pub fn with_looping(mut self, looping: bool) -> Self {
        self.looping = looping;
        self
    }

    pub fn with_speed(mut self, speed: f32) -> Self {
        self.speed = speed;
        self
    }

    pub fn to_animation_player(&self) -> AnimationPlayer {
        AnimationPlayer {
            clips: self.clips.clone(),
            current_clip: self.current_clip,
            time: self.time,
            speed: self.speed,
            looping: self.looping,
            playing: self.playing,
            play_all: self.play_all,
            node_index_to_entity: HashMap::new(),
            bone_name_to_entity: HashMap::new(),
            blend_from_clip: None,
            blend_from_time: 0.0,
            blend_factor: 1.0,
            blend_duration: 0.0,
        }
    }

    pub fn to_animation_player_with_resolved_clips(
        &self,
        asset_registry: Option<&super::registry::AssetRegistry>,
        warnings: &mut Vec<String>,
    ) -> AnimationPlayer {
        let mut clips = self.clips.clone();

        for clip_ref in &self.clip_refs {
            let path = if let Some(uuid) = clip_ref.uuid {
                asset_registry.and_then(|registry| asset_registry_resolve_uuid(registry, uuid))
            } else if let Some(ref path_str) = clip_ref.path {
                Some(std::path::PathBuf::from(path_str))
            } else if let Some(ref name) = clip_ref.name {
                asset_registry
                    .and_then(|registry| asset_registry_resolve_name(registry, name))
                    .and_then(|uuid| {
                        asset_registry
                            .and_then(|registry| asset_registry_resolve_uuid(registry, uuid))
                    })
            } else {
                None
            };

            if let Some(ref path) = path {
                match load_animation_clip_from_path(path) {
                    Ok(clip) => clips.push(clip),
                    Err(error) => warnings.push(format!(
                        "Failed to load animation clip from {}: {}",
                        path.display(),
                        error
                    )),
                }
            } else {
                warnings.push(format!(
                    "Could not resolve animation clip reference: {:?}",
                    clip_ref
                ));
            }
        }

        AnimationPlayer {
            clips,
            current_clip: self.current_clip,
            time: self.time,
            speed: self.speed,
            looping: self.looping,
            playing: self.playing,
            play_all: self.play_all,
            node_index_to_entity: HashMap::new(),
            bone_name_to_entity: HashMap::new(),
            blend_from_clip: None,
            blend_from_time: 0.0,
            blend_factor: 1.0,
            blend_duration: 0.0,
        }
    }
}

fn load_animation_clip_from_path(path: &std::path::Path) -> Result<AnimationClip, String> {
    let data = std::fs::read(path).map_err(|error| error.to_string())?;

    match path.extension().and_then(|extension| extension.to_str()) {
        Some("json") => serde_json::from_slice(&data).map_err(|error| error.to_string()),
        Some("bin") => {
            let decompressed =
                lz4_flex::decompress_size_prepended(&data).map_err(|error| error.to_string())?;
            bincode::deserialize(&decompressed).map_err(|error| error.to_string())
        }
        _ => bincode::deserialize(&data).map_err(|error| error.to_string()),
    }
}

impl From<&AnimationPlayer> for SceneAnimationPlayer {
    fn from(player: &AnimationPlayer) -> Self {
        Self {
            clips: player.clips.clone(),
            clip_refs: Vec::new(),
            current_clip: player.current_clip,
            time: player.time,
            speed: player.speed,
            looping: player.looping,
            playing: player.playing,
            play_all: player.play_all,
            // bone_name_to_node and node_index_to_node are populated by
            // the bulk-save post-pass (capture_animation_targets) and by
            // entity_to_scene_entity_with_uuids when called from the
            // editor's per-entity writeback, since both require an
            // entity-to-uuid map this trait impl has no access to.
            bone_name_to_node: HashMap::new(),
            node_index_to_node: HashMap::new(),
        }
    }
}