use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use crate::asset::handle::{AssetId, AssetPath, AssetRefCount, Handle, LoadState};
use crate::asset::loader::{Asset, AssetLoadError, BytesAsset, ScriptAsset, TextAsset, TomlAsset};
use crate::asset::registry::AssetRegistry;
struct PendingLoad {
path: String,
load_fn: Box<dyn FnOnce(&mut AssetRegistry, &str, &[u8]) -> Result<AssetId, AssetLoadError> + Send>,
reserved_id: AssetId,
}
pub struct AssetServer {
registry: AssetRegistry,
asset_root: PathBuf,
pending: VecDeque<PendingLoad>,
loading_count: usize,
loaded_count: usize,
ref_counts: HashMap<u64, std::sync::Arc<AssetRefCount>>,
}
impl AssetServer {
pub fn new() -> Self {
let mut registry = AssetRegistry::new();
registry.register::<TextAsset>();
registry.register::<BytesAsset>();
registry.register::<TomlAsset>();
registry.register::<ScriptAsset>();
Self {
registry,
asset_root: PathBuf::from("."),
pending: VecDeque::new(),
loading_count: 0,
loaded_count: 0,
ref_counts: HashMap::new(),
}
}
pub fn set_asset_root(&mut self, path: impl Into<PathBuf>) {
self.asset_root = path.into();
}
pub fn resolve_path(&self, relative: &str) -> PathBuf {
let p = Path::new(relative);
if p.is_absolute() {
p.to_path_buf()
} else {
self.asset_root.join(p)
}
}
pub fn asset_root(&self) -> &Path {
&self.asset_root
}
fn get_or_create_rc(&mut self, id: AssetId) -> std::sync::Arc<AssetRefCount> {
self.ref_counts
.entry(id.raw())
.or_insert_with(AssetRefCount::new)
.clone()
}
fn make_handle<T>(&mut self, id: AssetId) -> Handle<T> {
let rc = self.get_or_create_rc(id);
Handle::strong(id, rc)
}
pub fn load<T: Asset>(&mut self, path: &str) -> Handle<T> {
if let Some(storage) = self.registry.storage::<T>() {
if let Some(id) = storage.id_for_path(path) {
let state = storage.load_state(id);
if state.is_loaded() {
return self.make_handle(id);
}
}
}
let full_path = self.resolve_path(path);
let bytes = match std::fs::read(&full_path) {
Ok(b) => b,
Err(e) => {
let storage = self.registry.storage_or_create_mut::<T>();
let id = storage.reserve(path);
storage.mark_failed(id, format!("IO error reading '{}': {e}", full_path.display()));
return self.make_handle(id);
}
};
match self.registry.load_asset::<T>(path, &bytes) {
Ok(id) => {
self.loaded_count += 1;
self.make_handle(id)
}
Err(e) => {
let storage = self.registry.storage_or_create_mut::<T>();
let id = storage.reserve(path);
storage.mark_failed(id, e.to_string());
self.make_handle(id)
}
}
}
pub fn load_from_bytes<T: Asset>(&mut self, path: &str, bytes: &[u8]) -> Handle<T> {
match self.registry.load_asset::<T>(path, bytes) {
Ok(id) => {
self.loaded_count += 1;
self.make_handle(id)
}
Err(e) => {
let storage = self.registry.storage_or_create_mut::<T>();
let id = storage.reserve(path);
storage.mark_failed(id, e.to_string());
self.make_handle(id)
}
}
}
pub fn load_async<T: Asset>(&mut self, path: &str) -> Handle<T> {
if let Some(storage) = self.registry.storage::<T>() {
if let Some(id) = storage.id_for_path(path) {
if storage.load_state(id).is_loaded() {
return self.make_handle(id);
}
}
}
let storage = self.registry.storage_or_create_mut::<T>();
let id = storage.reserve(path);
self.loading_count += 1;
let path_owned = path.to_string();
self.pending.push_back(PendingLoad {
path: path_owned.clone(),
load_fn: Box::new(move |registry: &mut AssetRegistry, path: &str, bytes: &[u8]| {
registry.load_asset::<T>(path, bytes)
}),
reserved_id: id,
});
self.make_handle(id)
}
pub fn flush(&mut self) {
while let Some(pending) = self.pending.pop_front() {
let full_path = self.resolve_path(&pending.path);
let result = match std::fs::read(&full_path) {
Ok(bytes) => (pending.load_fn)(&mut self.registry, &pending.path, &bytes),
Err(e) => Err(AssetLoadError::Io {
path: full_path.display().to_string(),
message: e.to_string(),
}),
};
match result {
Ok(id) => {
if self.loading_count > 0 {
self.loading_count -= 1;
}
self.loaded_count += 1;
let _ = id; }
Err(e) => {
let _ = e;
if self.loading_count > 0 {
self.loading_count -= 1;
}
}
}
}
}
pub fn get<T: Asset>(&self, handle: &Handle<T>) -> Option<&T> {
self.registry.storage::<T>()?.get(handle.id())
}
pub fn get_mut<T: Asset>(&mut self, handle: &Handle<T>) -> Option<&mut T> {
self.registry.storage_mut::<T>()?.get_mut(handle.id())
}
pub fn get_load_state<T: Asset>(&self, handle: &Handle<T>) -> LoadState {
self.registry
.storage::<T>()
.map(|s| s.load_state(handle.id()))
.unwrap_or(LoadState::NotLoaded)
}
pub fn is_loaded<T: Asset>(&self, handle: &Handle<T>) -> bool {
self.get_load_state(handle).is_loaded()
}
pub fn is_loading<T: Asset>(&self, handle: &Handle<T>) -> bool {
self.get_load_state(handle).is_loading()
}
pub fn reload<T: Asset>(&mut self, handle: &Handle<T>) {
let path_opt: Option<String> = self
.registry
.storage::<T>()
.and_then(|s| {
s.paths()
.find(|p| s.id_for_path(p) == Some(handle.id()))
.map(|p| p.to_string())
});
if let Some(path) = path_opt {
let full_path = self.resolve_path(&path);
match std::fs::read(&full_path) {
Ok(bytes) => {
let ext = path.rsplit('.').next().unwrap_or("");
let ext_lower = ext.to_lowercase();
if let Some(loader) = self.registry.get_loader_for_extension(&ext_lower) {
let mut ctx = crate::asset::loader::LoadContext::new(&path);
match loader.load_bytes(&bytes, &path, &mut ctx) {
Ok(boxed) => {
if let Ok(asset) = boxed.downcast::<T>() {
if let Some(storage) = self.registry.storage_mut::<T>() {
storage.insert_at(handle.id(), *asset);
}
}
}
Err(_) => {}
}
}
}
Err(_) => {}
}
}
}
pub fn unload<T: Asset>(&mut self, handle: Handle<T>) {
if let Some(storage) = self.registry.storage_mut::<T>() {
if storage.contains(handle.id()) {
storage.remove(handle.id());
if self.loaded_count > 0 {
self.loaded_count -= 1;
}
}
}
self.ref_counts.remove(&handle.id().raw());
}
pub fn loaded_count(&self) -> usize {
self.loaded_count
}
pub fn loading_count(&self) -> usize {
self.loading_count
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn registry(&self) -> &AssetRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut AssetRegistry {
&mut self.registry
}
pub fn load_batch<T: Asset>(&mut self, paths: &[&str]) -> Vec<Handle<T>> {
paths.iter().map(|p| self.load::<T>(p)).collect()
}
pub fn load_async_batch<T: Asset>(&mut self, paths: &[&str]) -> Vec<Handle<T>> {
paths.iter().map(|p| self.load_async::<T>(p)).collect()
}
pub fn list_loadable_files(&self) -> Vec<PathBuf> {
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, out);
} else {
out.push(path);
}
}
}
}
let mut paths = Vec::new();
walk(&self.asset_root, &mut paths);
paths
.into_iter()
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.map(|e| self.registry.has_loader_for(e))
.unwrap_or(false)
})
.collect()
}
pub fn all_loaded(&self) -> bool {
self.pending.is_empty()
}
}
impl Default for AssetServer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asset::loader::TextAsset;
use std::io::Write;
fn temp_txt_file(content: &str) -> (tempfile_helper::TempDir, PathBuf) {
let dir = tempfile_helper::TempDir::new();
let path = dir.path().join("test.txt");
std::fs::write(&path, content).unwrap();
(dir, path)
}
mod tempfile_helper {
use std::path::{Path, PathBuf};
pub struct TempDir {
path: PathBuf,
}
impl TempDir {
pub fn new() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let t = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_nanos();
let path = std::env::temp_dir().join(format!("proof_asset_test_{t}"));
std::fs::create_dir_all(&path).unwrap();
Self { path }
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
}
#[test]
fn load_from_bytes_text() {
let mut server = AssetServer::new();
let handle = server.load_from_bytes::<TextAsset>("readme.txt", b"hello world");
assert!(server.is_loaded(&handle));
assert_eq!(server.get(&handle).unwrap().text, "hello world");
}
#[test]
fn get_load_state_not_loaded() {
let server = AssetServer::new();
let handle: Handle<TextAsset> = Handle::weak(AssetId::new(99, 99));
assert_eq!(server.get_load_state(&handle), LoadState::NotLoaded);
}
#[test]
fn load_from_bytes_twice_same_handle() {
let mut server = AssetServer::new();
let h1 = server.load_from_bytes::<TextAsset>("a.txt", b"first");
let h2 = server.load_from_bytes::<TextAsset>("a.txt", b"second");
assert_eq!(h1.id(), h2.id());
assert_eq!(server.get(&h2).unwrap().text, "second");
}
#[test]
fn unload_removes_asset() {
let mut server = AssetServer::new();
let handle = server.load_from_bytes::<TextAsset>("bye.txt", b"bye");
assert!(server.is_loaded(&handle));
let weak = handle.clone_weak();
server.unload(handle);
assert!(!server.is_loaded(&weak));
assert!(server.get(&weak).is_none());
}
#[test]
fn load_batch() {
let mut server = AssetServer::new();
server.load_from_bytes::<TextAsset>("x.txt", b"x");
server.load_from_bytes::<TextAsset>("y.txt", b"y");
let handles = server.load_batch::<TextAsset>(&["x.txt", "y.txt"]);
assert_eq!(handles.len(), 2);
}
#[test]
fn asset_server_resolve_path() {
let mut server = AssetServer::new();
server.set_asset_root("/game/assets");
let resolved = server.resolve_path("fonts/mono.ttf");
assert_eq!(resolved, PathBuf::from("/game/assets/fonts/mono.ttf"));
}
#[test]
fn loaded_count_increments() {
let mut server = AssetServer::new();
assert_eq!(server.loaded_count(), 0);
server.load_from_bytes::<TextAsset>("a.txt", b"a");
server.load_from_bytes::<TextAsset>("b.txt", b"b");
assert_eq!(server.loaded_count(), 2);
}
#[test]
fn load_sync_from_disk() {
let dir = {
use std::time::{SystemTime, UNIX_EPOCH};
let t = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_nanos();
let d = std::env::temp_dir().join(format!("proof_asset_disk_{t}"));
std::fs::create_dir_all(&d).unwrap();
d
};
let file_path = dir.join("disk.txt");
std::fs::write(&file_path, b"disk content").unwrap();
let mut server = AssetServer::new();
server.set_asset_root(&dir);
let handle = server.load::<TextAsset>("disk.txt");
assert!(server.is_loaded(&handle), "state: {:?}", server.get_load_state(&handle));
assert_eq!(server.get(&handle).unwrap().text, "disk content");
let _ = std::fs::remove_dir_all(&dir);
}
}