use std::collections::HashMap;
use bevy_asset::{AssetLoader, LoadContext, io::Reader};
use bevy_reflect::TypePath;
use crate::asset::{BrinkStoryAsset, emit_story_assets};
#[derive(Default, TypePath)]
pub struct InkLoader;
#[derive(Debug, thiserror::Error)]
pub enum InkLoaderError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("read asset: {0}")]
ReadAsset(#[from] bevy_asset::ReadAssetBytesError),
#[error("source not valid UTF-8: {0}")]
InvalidUtf8(#[from] std::string::FromUtf8Error),
#[error("entry path missing or non-UTF-8")]
BadEntryPath,
#[error("compile: {0}")]
Compile(#[from] brink_compiler::CompileError),
#[error("link error: {0}")]
Link(#[from] brink_runtime::RuntimeError),
}
fn resolve_include_path(from_file: &str, include_path: &str) -> String {
match from_file.rfind('/') {
Some(i) => format!("{}/{include_path}", &from_file[..i]),
None => include_path.to_string(),
}
}
impl AssetLoader for InkLoader {
type Asset = BrinkStoryAsset;
type Settings = ();
type Error = InkLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let entry_path = load_context
.path()
.path()
.to_str()
.ok_or(InkLoaderError::BadEntryPath)?
.to_string();
let mut entry_bytes = Vec::new();
reader.read_to_end(&mut entry_bytes).await?;
let entry_source = String::from_utf8(entry_bytes)?;
let mut sources: HashMap<String, String> = HashMap::new();
let mut queue: Vec<String> = brink_syntax::extract_includes(&entry_source)
.into_iter()
.map(|inc| resolve_include_path(&entry_path, &inc))
.collect();
sources.insert(entry_path.clone(), entry_source);
while let Some(path) = queue.pop() {
if sources.contains_key(&path) {
continue;
}
let bytes = load_context.read_asset_bytes(path.clone()).await?;
let source = String::from_utf8(bytes)?;
for inc in brink_syntax::extract_includes(&source) {
let resolved = resolve_include_path(&path, &inc);
if !sources.contains_key(&resolved) {
queue.push(resolved);
}
}
sources.insert(path, source);
}
let output = brink_compiler::compile(&entry_path, |p| {
sources.get(p).cloned().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("{p}: not in pre-fetched source cache"),
)
})
})?;
let (program, tables) = brink_runtime::link(&output.data)?;
Ok(emit_story_assets(load_context, program, tables))
}
fn extensions(&self) -> &[&str] {
&["ink"]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_with_directory_prefix() {
assert_eq!(
resolve_include_path("src/main.ink", "utils.ink"),
"src/utils.ink"
);
}
#[test]
fn resolves_without_directory() {
assert_eq!(resolve_include_path("story.ink", "other.ink"), "other.ink");
}
#[test]
fn resolves_nested_directory() {
assert_eq!(resolve_include_path("a/b/c.ink", "d.ink"), "a/b/d.ink");
}
}