use bevy::asset::LoadContext;
use bevy::platform::collections::HashMap;
use std::{
io::{Cursor, Error as IoError, ErrorKind, Read},
path::{Path, PathBuf},
sync::Arc,
};
pub(crate) struct BytesResourceReader<'a> {
bytes: Arc<[u8]>,
cache: &'a HashMap<PathBuf, Vec<u8>>,
}
impl<'a> BytesResourceReader<'a> {
pub(crate) fn new(bytes: &[u8], cache: &'a HashMap<PathBuf, Vec<u8>>) -> Self {
Self {
bytes: Arc::from(bytes),
cache,
}
}
}
impl tiled::ResourceReader for BytesResourceReader<'_> {
type Resource = Box<dyn Read>;
type Error = IoError;
fn read_from(&mut self, path: &Path) -> std::result::Result<Self::Resource, Self::Error> {
if let Some(extension) = path.extension() {
if extension == "tsx" || extension == "tx" {
if let Some(data) = self.cache.get(&normalize_path(path)) {
return Ok(Box::new(Cursor::new(data.clone())));
}
return Err(IoError::new(
ErrorKind::NotFound,
format!(
"External tileset/template '{}' not found in cache. \
For WASM builds, all external tilesets must be pre-loaded.",
path.display()
),
));
}
}
Ok(Box::new(Cursor::new(self.bytes.clone())))
}
}
pub(crate) fn extract_external_paths(xml_content: &[u8]) -> Vec<PathBuf> {
let mut paths = Vec::new();
let content = String::from_utf8_lossy(xml_content);
for line in content.lines() {
for marker in ["source=\"", "template=\""] {
if let Some(start) = line.find(marker) {
let rest = &line[start + marker.len()..];
if let Some(end) = rest.find('"') {
let path = &rest[..end];
if path.ends_with(".tsx") || path.ends_with(".tx") {
paths.push(PathBuf::from(path));
}
}
}
}
}
paths
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {}
component => components.push(component),
}
}
components.iter().collect()
}
fn resolve_relative_path(asset_path: &Path, relative_path: &Path) -> PathBuf {
normalize_path(&asset_path.parent().map_or_else(
|| relative_path.to_path_buf(),
|parent| parent.join(relative_path),
))
}
pub(crate) async fn preload_external_resources(
xml_content: &[u8],
load_context: &mut LoadContext<'_>,
) -> HashMap<PathBuf, Vec<u8>> {
let mut cache = HashMap::default();
let paths = extract_external_paths(xml_content);
let asset_path = load_context.path().path().to_path_buf();
for relative_path in paths {
let resolved_path = resolve_relative_path(&asset_path, &relative_path);
let cache_key = resolved_path.clone();
match load_context.read_asset_bytes(resolved_path.clone()).await {
Ok(bytes) => {
let nested = extract_external_paths(&bytes);
for nested_relative in nested {
let nested_resolved = resolve_relative_path(&resolved_path, &nested_relative);
let nested_cache_key = nested_resolved.clone();
if !cache.contains_key(&nested_cache_key) {
if let Ok(nested_bytes) =
load_context.read_asset_bytes(nested_resolved).await
{
cache.insert(nested_cache_key, nested_bytes);
}
}
}
cache.insert(cache_key, bytes);
}
Err(e) => {
log::warn!(
"Failed to pre-load external resource '{}': {}",
relative_path.display(),
e
);
}
}
}
cache
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_nested_template_from_equivalent_normalized_cache_path() {
let template_path = PathBuf::from("../templates/tree_base_collision.tx");
assert_eq!(
extract_external_paths(br#"<object template="../templates/tree_base_collision.tx"/>"#),
vec![template_path]
);
let expected = b"nested template";
let cache = HashMap::from([(
PathBuf::from("tiled/templates/tree_base_collision.tx"),
expected.to_vec(),
)]);
let mut reader = BytesResourceReader::new(b"", &cache);
let mut resource = tiled::ResourceReader::read_from(
&mut reader,
Path::new("tiled/grassland/../tilesets/../templates/tree_base_collision.tx"),
)
.expect("equivalent external resource paths should share one cache entry");
let mut actual = Vec::new();
resource.read_to_end(&mut actual).unwrap();
assert_eq!(actual, expected);
}
}