use std::sync::Arc;
use bevy::asset::{AssetLoader, AssetPath, LoadContext, io::Reader};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use spine_runtime::data::SkeletonData;
use spine_runtime::load::{AtlasAttachmentLoader, BinaryError, SkeletonBinary};
use crate::asset::atlas_loader::SpineAtlasAsset;
#[derive(Asset, TypePath, Debug)]
pub struct SpineSkeletonAsset {
pub data: Arc<SkeletonData>,
pub atlas: Handle<SpineAtlasAsset>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct SpineSkeletonLoaderSettings {
pub atlas_path: Option<String>,
pub scale: Option<f32>,
}
#[derive(Default, TypePath)]
pub struct SpineSkeletonLoader;
#[derive(Debug, Error)]
pub enum SpineSkeletonLoaderError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("could not derive atlas path from skeleton path {0:?}")]
AtlasPathDerivation(String),
#[error("failed to load companion atlas {0:?}: {1}")]
AtlasLoad(String, String),
#[error("binary skeleton parse error: {0}")]
Parse(#[from] BinaryError),
#[error("asset path parse error: {0}")]
Path(#[from] bevy::asset::ParseAssetPathError),
}
impl From<super::AtlasDeriveError> for SpineSkeletonLoaderError {
fn from(e: super::AtlasDeriveError) -> Self {
match e {
super::AtlasDeriveError::BadStem(s) => Self::AtlasPathDerivation(s),
super::AtlasDeriveError::Parse(e) => Self::Path(e),
}
}
}
impl AssetLoader for SpineSkeletonLoader {
type Asset = SpineSkeletonAsset;
type Settings = SpineSkeletonLoaderSettings;
type Error = SpineSkeletonLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let atlas_path = resolve_atlas_path(load_context.path(), settings.atlas_path.as_deref())?;
let atlas_handle: Handle<SpineAtlasAsset> = load_context.load(atlas_path.clone());
let loaded_atlas = load_context
.load_builder()
.load_value::<SpineAtlasAsset>(atlas_path.clone())
.await
.map_err(|e| {
SpineSkeletonLoaderError::AtlasLoad(atlas_path.to_string(), e.to_string())
})?;
let atlas_asset = loaded_atlas.get();
let mut attachment_loader = AtlasAttachmentLoader::new(&atlas_asset.atlas);
let mut binary = SkeletonBinary::with_loader(&mut attachment_loader);
if let Some(scale) = settings.scale {
binary = binary.with_scale(scale);
}
let data = binary.read(&bytes)?;
Ok(SpineSkeletonAsset {
data: Arc::new(data),
atlas: atlas_handle,
})
}
fn extensions(&self) -> &[&str] {
&["skel"]
}
}
fn resolve_atlas_path(
skel_path: &AssetPath<'static>,
override_path: Option<&str>,
) -> Result<AssetPath<'static>, SpineSkeletonLoaderError> {
super::derive_atlas_path(skel_path, override_path).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn make_path(p: &str) -> AssetPath<'static> {
AssetPath::from(PathBuf::from(p))
}
#[test]
fn strips_pro_suffix() {
let skel = make_path("rigs/spineboy/export/spineboy-pro.skel");
let atlas = resolve_atlas_path(&skel, None).unwrap();
assert_eq!(
atlas.path().to_str(),
Some("rigs/spineboy/export/spineboy.atlas")
);
}
#[test]
fn strips_ess_suffix() {
let skel = make_path("rigs/spineboy-ess.skel");
let atlas = resolve_atlas_path(&skel, None).unwrap();
assert_eq!(atlas.path().to_str(), Some("rigs/spineboy.atlas"));
}
#[test]
fn keeps_unsuffixed_stem() {
let skel = make_path("rigs/raptor.skel");
let atlas = resolve_atlas_path(&skel, None).unwrap();
assert_eq!(atlas.path().to_str(), Some("rigs/raptor.atlas"));
}
#[test]
fn honours_override() {
let skel = make_path("rigs/spineboy-pro.skel");
let atlas = resolve_atlas_path(&skel, Some("packs/hero.atlas")).unwrap();
assert_eq!(atlas.path().to_str(), Some("packs/hero.atlas"));
}
}