use crate::ecs::world::World;
use crate::ecs::world::commands::WorldCommand;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock};
static ASSET_SEARCH_PATHS: OnceLock<Vec<String>> = OnceLock::new();
pub fn set_asset_search_paths(paths: Vec<String>) {
let _ = ASSET_SEARCH_PATHS.set(paths);
}
pub fn get_asset_search_paths() -> &'static [String] {
ASSET_SEARCH_PATHS.get().map_or(&[], |v| v.as_slice())
}
#[derive(Default)]
pub struct TextureLoadQueue {
pending: VecDeque<TextureLoadRequest>,
in_flight: Vec<String>,
completed: Vec<LoadedTexture>,
failed: usize,
}
pub struct TextureLoadRequest {
pub url: String,
pub name: String,
}
pub struct LoadedTexture {
pub name: String,
pub rgba_data: Vec<u8>,
pub width: u32,
pub height: u32,
}
impl TextureLoadQueue {
pub fn new() -> Self {
Self::default()
}
pub fn queue_texture(&mut self, name: String, url: String) {
if !self.in_flight.contains(&name)
&& !self.pending.iter().any(|r| r.name == name)
&& !self.completed.iter().any(|t| t.name == name)
{
self.pending.push_back(TextureLoadRequest { url, name });
}
}
pub fn take_completed(&mut self) -> Vec<LoadedTexture> {
std::mem::take(&mut self.completed)
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty() || !self.in_flight.is_empty()
}
pub fn pending_count(&self) -> usize {
self.pending.len() + self.in_flight.len()
}
pub fn failed_count(&self) -> usize {
self.failed
}
pub fn processed_count(&self) -> usize {
self.completed.len() + self.failed
}
}
pub type SharedTextureQueue = Arc<Mutex<TextureLoadQueue>>;
pub fn create_shared_queue() -> SharedTextureQueue {
Arc::new(Mutex::new(TextureLoadQueue::new()))
}
#[cfg(target_arch = "wasm32")]
pub fn process_texture_queue(queue: &SharedTextureQueue, max_concurrent: usize) {
let requests: Vec<TextureLoadRequest> = {
let mut q = queue.lock().unwrap();
let available_slots = max_concurrent.saturating_sub(q.in_flight.len());
let mut requests = Vec::new();
for _ in 0..available_slots {
if let Some(req) = q.pending.pop_front() {
q.in_flight.push(req.name.clone());
requests.push(req);
}
}
requests
};
for request in requests {
let queue_clone = Arc::clone(queue);
let name = request.name.clone();
ehttp::fetch(
ehttp::Request::get(&request.url),
move |result: ehttp::Result<ehttp::Response>| {
let mut q = queue_clone.lock().unwrap();
q.in_flight.retain(|n| n != &name);
match result {
Ok(response) => {
if response.ok {
if let Ok(img) = image::load_from_memory(&response.bytes) {
let rgba = img.to_rgba8();
let (width, height) = rgba.dimensions();
q.completed.push(LoadedTexture {
name,
rgba_data: rgba.into_raw(),
width,
height,
});
} else {
tracing::warn!("Failed to decode image: {}", name);
q.failed += 1;
}
} else {
tracing::warn!(
"HTTP error loading texture {}: {}",
name,
response.status
);
q.failed += 1;
}
}
Err(error) => {
tracing::warn!("Failed to fetch texture {}: {}", name, error);
q.failed += 1;
}
}
},
);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn process_texture_queue(queue: &SharedTextureQueue, _max_concurrent: usize) {
use std::path::Path;
let requests: Vec<TextureLoadRequest> = {
let mut q = queue.lock().unwrap();
std::mem::take(&mut q.pending).into()
};
for request in requests {
let path = Path::new(&request.url);
let bytes = if path.exists() {
std::fs::read(path)
} else {
find_texture_file(&request.url)
};
match bytes {
Ok(bytes) => {
if let Ok(img) = image::load_from_memory(&bytes) {
let rgba = img.to_rgba8();
let (width, height) = rgba.dimensions();
let mut q = queue.lock().unwrap();
q.completed.push(LoadedTexture {
name: request.name,
rgba_data: rgba.into_raw(),
width,
height,
});
} else {
tracing::warn!("Failed to decode image: {}", request.name);
let mut q = queue.lock().unwrap();
q.failed += 1;
}
}
Err(error) => {
tracing::warn!("Failed to read texture {}: {}", request.name, error);
let mut q = queue.lock().unwrap();
q.failed += 1;
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn find_texture_file(url: &str) -> std::io::Result<Vec<u8>> {
use std::path::Path;
let candidate = Path::new(url);
if candidate.exists() {
return std::fs::read(candidate);
}
for prefix in get_asset_search_paths() {
let candidate = Path::new(prefix).join(url);
if candidate.exists() {
return std::fs::read(&candidate);
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Texture not found: {}", url),
))
}
#[cfg(all(feature = "file_watcher", not(target_arch = "wasm32")))]
fn resolve_texture_path(name: &str) -> Option<std::path::PathBuf> {
use std::path::Path;
let candidate = Path::new(name);
if candidate.exists() {
return Some(candidate.to_path_buf());
}
for prefix in get_asset_search_paths() {
let candidate = Path::new(prefix).join(name);
if candidate.exists() {
return Some(candidate);
}
}
None
}
pub fn queue_texture_from_path(queue: &SharedTextureQueue, path: &str) {
let normalized = path.replace('\\', "/");
let name = normalized.clone();
#[cfg(target_arch = "wasm32")]
let url = {
if let Some(stripped) = normalized.strip_prefix("assets/") {
stripped.to_string()
} else {
normalized
}
};
#[cfg(not(target_arch = "wasm32"))]
let url = normalized;
if let Ok(mut q) = queue.lock() {
q.queue_texture(name, url);
}
}
#[derive(Default, Clone, Copy)]
pub struct AssetLoadingState {
pub total_textures: usize,
pub loaded_textures: usize,
pub failed_textures: usize,
}
impl AssetLoadingState {
pub fn new(total_textures: usize) -> Self {
Self {
total_textures,
loaded_textures: 0,
failed_textures: 0,
}
}
pub fn progress(&self) -> f32 {
if self.total_textures == 0 {
return 1.0;
}
(self.loaded_textures + self.failed_textures) as f32 / self.total_textures as f32
}
pub fn is_complete(&self) -> bool {
self.total_textures > 0
&& (self.loaded_textures + self.failed_textures) >= self.total_textures
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum AssetLoadingStatus {
#[default]
Loading,
Complete,
}
pub fn process_and_load_textures(
queue: &SharedTextureQueue,
world: &mut World,
loading_state: &mut AssetLoadingState,
max_concurrent: usize,
) -> AssetLoadingStatus {
process_texture_queue(queue, max_concurrent);
if let Ok(mut queue_guard) = queue.lock() {
let completed = queue_guard.take_completed();
let failed = queue_guard.failed_count();
loading_state.loaded_textures += completed.len();
loading_state.failed_textures = failed;
for loaded in completed {
#[cfg(all(feature = "file_watcher", not(target_arch = "wasm32")))]
{
let resolved_path = resolve_texture_path(&loaded.name);
if let Some(path) = resolved_path {
world
.resources
.asset_watcher
.track_texture(loaded.name.clone(), path);
}
}
world.queue_command(WorldCommand::LoadTexture {
name: loaded.name,
rgba_data: loaded.rgba_data,
width: loaded.width,
height: loaded.height,
});
}
let pending = queue_guard.has_pending();
let all_processed =
(loading_state.loaded_textures + failed) >= loading_state.total_textures;
if !pending && all_processed {
if failed > 0 {
tracing::warn!(
"Asset loading complete: {} textures loaded, {} failed",
loading_state.loaded_textures,
failed
);
} else if loading_state.loaded_textures > 0 {
tracing::info!(
"Asset loading complete: {} textures loaded",
loading_state.loaded_textures
);
}
return AssetLoadingStatus::Complete;
}
}
AssetLoadingStatus::Loading
}