Asset database file ops + script-file creation helpers
Groundwork for the Stage-10 editor-UX batch (file explorer, New Script button), all pure logic proven by unit tests: - `AssetDatabase::move_asset` / `move_folder` / `delete_asset`: perform the disk operation and the uid-map update together, so renaming or moving an asset (or a whole folder) keeps its uid and every saved `AssetRef` resolving. Kinds re-classify from the new path; `..` and existing destinations are refused via the new `AssetDbError` enum; deleted uids are never reused. - Editor `assets.rs`: `script_template` (compiles under the sandboxed ScriptEngine — covered by a test), `sanitize_script_stem`, and `create_script_file` which writes a collision-free `assets/scripts/<stem>.rhai` and returns the relative path for database registration. - docs/assets.md: new "File operations" section + the missing Script row in the typed-folder table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -164,10 +164,34 @@ breaking references, since the uid travels with the file in the manifest.
|
||||
| `Model` | `models/` | `gltf`, `glb`, `obj` |
|
||||
| `Audio` | `audio/` | `wav`, `ogg`, `mp3`, `flac` |
|
||||
| `Ui` | `ui/` | (recognised by folder — shares `.ron` with scenes) |
|
||||
| `Script` | `scripts/` | `rhai` |
|
||||
|
||||
`AssetKind::classify(path)` infers the kind: the leading folder wins, with the
|
||||
file extension as a fallback for files dropped directly in `assets/`.
|
||||
|
||||
### File operations — rename, move, delete
|
||||
|
||||
The database also *performs* file reorganisation, so the editor's file explorer
|
||||
(and any tool) can rename or move assets **without breaking saved references**
|
||||
— the disk operation and the uid map are updated together:
|
||||
|
||||
```rust
|
||||
# use oxide_engine::asset::{AssetDatabase, AssetUid};
|
||||
# fn demo(db: &mut AssetDatabase, uid: AssetUid) -> Result<(), oxide_engine::asset::AssetDbError> {
|
||||
db.move_asset(uid, "textures/env/brick.png")?; // rename/move; uid unchanged
|
||||
db.move_folder("textures/env", "textures/world")?; // every entry under it follows
|
||||
db.delete_asset(uid)?; // file + entry; uid never reused
|
||||
db.save()?; // persist the new paths
|
||||
# Ok(())
|
||||
# }
|
||||
```
|
||||
|
||||
All three refuse to touch anything outside `assets/` (`..` is rejected) and
|
||||
refuse to overwrite an existing destination ([`AssetDbError`] has a variant per
|
||||
failure). Kinds are re-classified from the new path, since a move can change
|
||||
the typed folder. Deleting drops the entry but never reuses its uid — a
|
||||
dangling reference stays dangling instead of silently pointing at a new file.
|
||||
|
||||
### Asset-reference fields in the inspector
|
||||
|
||||
A component points at an asset with an [`AssetRef<T>`] field — **not** a live
|
||||
@@ -209,6 +233,7 @@ projects are seeded with a bundled default UI font (Inter, SIL OFL) under
|
||||
`fonts/`, referenced by its project-relative path like any other asset.
|
||||
|
||||
[`AssetDatabase`]: ../engine/src/asset/database.rs
|
||||
[`AssetDbError`]: ../engine/src/asset/database.rs
|
||||
[`AssetKind`]: ../engine/src/asset/database.rs
|
||||
[`AssetUid`]: ../engine/src/asset/database.rs
|
||||
[`AssetRef<T>`]: ../engine/src/asset/database.rs
|
||||
|
||||
+123
-2
@@ -1,5 +1,7 @@
|
||||
//! Bundled editor assets — locating the shared `assets/` tree and seeding a
|
||||
//! new project's default content (currently the default UI font).
|
||||
//! Bundled editor assets — locating the shared `assets/` tree, seeding a new
|
||||
//! project's default content (currently the default UI font), and creating new
|
||||
//! script files from the built-in template (the inspector's "New Script"
|
||||
//! button).
|
||||
//!
|
||||
//! The editor ships a small set of shared assets (icons, the default UI font, …)
|
||||
//! installed by `install.sh` to `$PREFIX/share/oxide/assets`. At runtime we have
|
||||
@@ -91,6 +93,79 @@ pub fn seed_default_font(project_assets_dir: &Path) -> std::io::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The `.rhai` source the "New Script" button writes, personalised with the
|
||||
/// script's file stem so the Console output identifies which script speaks.
|
||||
///
|
||||
/// Kept to the two lifecycle hooks `docs/scripting.md` teaches first; the
|
||||
/// `update` body ships commented out so a freshly created script visibly runs
|
||||
/// (the `init` print) without moving anything until the author opts in.
|
||||
pub fn script_template(stem: &str) -> String {
|
||||
format!(
|
||||
r#"// {stem}.rhai — attached via a Script component.
|
||||
//
|
||||
// Top-level statements run once when the script (re)starts. `init()` runs
|
||||
// once after them; `update(dt)` runs every frame (dt = seconds).
|
||||
|
||||
fn init() {{
|
||||
print("{stem}: init");
|
||||
}}
|
||||
|
||||
fn update(dt) {{
|
||||
// e.g. rotate_y(dt * 1.5);
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Reduces a typed script name to a safe file stem: keeps ASCII alphanumerics,
|
||||
/// `-` and `_`, folds anything else (spaces, punctuation, Unicode) to `_`,
|
||||
/// collapses runs, trims the ends, and drops a trailing `.rhai` the user may
|
||||
/// have typed. An unusable input yields `"new_script"`.
|
||||
pub fn sanitize_script_stem(name: &str) -> String {
|
||||
let trimmed = name.trim();
|
||||
let trimmed = trimmed.strip_suffix(".rhai").unwrap_or(trimmed);
|
||||
let mut stem = String::with_capacity(trimmed.len());
|
||||
for c in trimmed.chars() {
|
||||
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
|
||||
stem.push(c);
|
||||
} else if !stem.ends_with('_') {
|
||||
stem.push('_');
|
||||
}
|
||||
}
|
||||
let stem = stem.trim_matches('_');
|
||||
if stem.is_empty() {
|
||||
"new_script".to_owned()
|
||||
} else {
|
||||
stem.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new script file under `<assets_dir>/scripts/` from the template,
|
||||
/// returning its assets-relative path (e.g. `"scripts/my_script.rhai"`) for
|
||||
/// registration in the [`AssetDatabase`](oxide_engine::asset::AssetDatabase).
|
||||
///
|
||||
/// The desired name is [sanitized](sanitize_script_stem); a taken name gets a
|
||||
/// numeric suffix (`stem_2`, `stem_3`, …) instead of failing or overwriting,
|
||||
/// so the button always succeeds on a writable project.
|
||||
pub fn create_script_file(assets_dir: &Path, desired_name: &str) -> std::io::Result<String> {
|
||||
use oxide_engine::asset::AssetKind;
|
||||
|
||||
let stem = sanitize_script_stem(desired_name);
|
||||
let dir = assets_dir.join(AssetKind::Script.folder());
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let mut candidate = stem.clone();
|
||||
let mut n = 1;
|
||||
while dir.join(format!("{candidate}.rhai")).exists() {
|
||||
n += 1;
|
||||
candidate = format!("{stem}_{n}");
|
||||
}
|
||||
std::fs::write(
|
||||
dir.join(format!("{candidate}.rhai")),
|
||||
script_template(&candidate),
|
||||
)?;
|
||||
Ok(format!("{}/{candidate}.rhai", AssetKind::Script.folder()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -124,4 +199,50 @@ mod tests {
|
||||
|
||||
std::fs::remove_dir_all(tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_covers_typed_names() {
|
||||
assert_eq!(sanitize_script_stem("spin"), "spin");
|
||||
assert_eq!(sanitize_script_stem(" My Cool Script! "), "My_Cool_Script");
|
||||
assert_eq!(sanitize_script_stem("door.rhai"), "door");
|
||||
assert_eq!(sanitize_script_stem("a//b\\c"), "a_b_c");
|
||||
assert_eq!(sanitize_script_stem("čárka"), "rka");
|
||||
// Unusable inputs fall back rather than producing "" or "_".
|
||||
assert_eq!(sanitize_script_stem(""), "new_script");
|
||||
assert_eq!(sanitize_script_stem("!!!"), "new_script");
|
||||
assert_eq!(sanitize_script_stem(".rhai"), "new_script");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_script_writes_template_and_dodges_collisions() {
|
||||
let mut tmp = std::env::temp_dir();
|
||||
tmp.push(format!("oxide_newscript_{}", std::process::id()));
|
||||
let assets = tmp.join("assets");
|
||||
std::fs::create_dir_all(&assets).unwrap();
|
||||
|
||||
let rel = create_script_file(&assets, "door opener").unwrap();
|
||||
assert_eq!(rel, "scripts/door_opener.rhai");
|
||||
let text = std::fs::read_to_string(assets.join(&rel)).unwrap();
|
||||
assert!(text.contains("fn update(dt)"));
|
||||
|
||||
// Same name again: suffixed, nothing overwritten.
|
||||
let rel2 = create_script_file(&assets, "door opener").unwrap();
|
||||
assert_eq!(rel2, "scripts/door_opener_2.rhai");
|
||||
let rel3 = create_script_file(&assets, "door opener").unwrap();
|
||||
assert_eq!(rel3, "scripts/door_opener_3.rhai");
|
||||
|
||||
std::fs::remove_dir_all(tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_compiles_in_the_script_engine() {
|
||||
// The template must never ship a syntax error: compile it exactly as
|
||||
// the runtime would.
|
||||
let asset = oxide_script::ScriptAsset::from_source(
|
||||
"new_script.rhai",
|
||||
script_template("new_script"),
|
||||
);
|
||||
let engine = oxide_script::ScriptEngine::new();
|
||||
engine.compile(&asset).expect("template compiles");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,30 @@ pub fn asset_ref_target(type_name: &str) -> Option<&str> {
|
||||
Some(inner.rsplit("::").next().unwrap_or(inner).trim())
|
||||
}
|
||||
|
||||
/// Errors from [`AssetDatabase`] file operations ([`move_asset`], [`delete_asset`],
|
||||
/// [`move_folder`]) — the ops that touch both the on-disk file *and* the uid map.
|
||||
///
|
||||
/// [`move_asset`]: AssetDatabase::move_asset
|
||||
/// [`delete_asset`]: AssetDatabase::delete_asset
|
||||
/// [`move_folder`]: AssetDatabase::move_folder
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AssetDbError {
|
||||
/// The uid is not recorded in the database.
|
||||
#[error("unknown asset uid {0:?}")]
|
||||
UnknownUid(AssetUid),
|
||||
/// The destination path is already taken, in the database or on disk.
|
||||
/// Nothing was changed; pick another name.
|
||||
#[error("destination already exists: {0}")]
|
||||
DestinationExists(String),
|
||||
/// The path is empty, escapes the assets directory (`..`), or would move a
|
||||
/// folder into itself.
|
||||
#[error("invalid assets-relative path: {0:?}")]
|
||||
InvalidPath(String),
|
||||
/// The underlying filesystem operation failed.
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// A stable, per-project identifier for one asset.
|
||||
///
|
||||
/// Unlike [`AssetId`](super::AssetId) — which is process-unique and changes
|
||||
@@ -507,6 +531,126 @@ impl AssetDatabase {
|
||||
Some(server.load::<T>(path))
|
||||
}
|
||||
|
||||
// --- file operations (disk + uid map together) --------------------------
|
||||
//
|
||||
// These keep the uid attached to its asset across renames/moves — the whole
|
||||
// point of the uid layer — so the editor's file explorer can reorganise a
|
||||
// project without breaking any saved `AssetRef`. Deletion drops the entry
|
||||
// but never reuses its uid (the allocator only counts up).
|
||||
|
||||
/// Renames/moves the asset `uid` to `new_relative_path` (relative to
|
||||
/// `assets/`), on disk and in the map, **keeping its uid** so saved
|
||||
/// references still resolve. The kind is re-classified from the new path.
|
||||
/// Parent directories are created as needed; moving onto an existing path
|
||||
/// is refused. A no-op if the path is unchanged.
|
||||
///
|
||||
/// Call [`save`](Self::save) afterwards to persist the new path.
|
||||
pub fn move_asset(
|
||||
&mut self,
|
||||
uid: AssetUid,
|
||||
new_relative_path: impl AsRef<str>,
|
||||
) -> Result<(), AssetDbError> {
|
||||
let entry = self.by_uid.get(&uid).ok_or(AssetDbError::UnknownUid(uid))?;
|
||||
let new_path = checked_relative(new_relative_path.as_ref())?;
|
||||
if new_path == entry.path {
|
||||
return Ok(());
|
||||
}
|
||||
if self.by_path.contains_key(&new_path) {
|
||||
return Err(AssetDbError::DestinationExists(new_path));
|
||||
}
|
||||
let assets_dir = self.assets_dir();
|
||||
let from_abs = assets_dir.join(entry.path.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
let to_abs = assets_dir.join(new_path.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
if to_abs.exists() {
|
||||
return Err(AssetDbError::DestinationExists(new_path));
|
||||
}
|
||||
if let Some(parent) = to_abs.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::rename(&from_abs, &to_abs)?;
|
||||
|
||||
let old_path = entry.path.clone();
|
||||
self.by_path.remove(&old_path);
|
||||
self.by_path.insert(new_path.clone(), uid);
|
||||
let entry = self.by_uid.get_mut(&uid).expect("entry checked above");
|
||||
entry.kind = AssetKind::classify(&new_path);
|
||||
entry.path = new_path;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes the asset `uid`: removes its file from disk and drops its entry.
|
||||
/// A file already missing from disk is fine (the entry is still dropped);
|
||||
/// the uid is never reused. Call [`save`](Self::save) afterwards.
|
||||
pub fn delete_asset(&mut self, uid: AssetUid) -> Result<(), AssetDbError> {
|
||||
let entry = self.by_uid.get(&uid).ok_or(AssetDbError::UnknownUid(uid))?;
|
||||
let abs = self
|
||||
.assets_dir()
|
||||
.join(entry.path.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
match std::fs::remove_file(&abs) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
let path = entry.path.clone();
|
||||
self.by_uid.remove(&uid);
|
||||
self.by_path.remove(&path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renames/moves the folder `from` to `to` (both relative to `assets/`),
|
||||
/// on disk and in the map: every entry under `from/` is rewritten to live
|
||||
/// under `to/`, **keeping its uid** (kinds are re-classified — moving out
|
||||
/// of a typed folder can change them). Refuses to overwrite an existing
|
||||
/// destination or to move a folder into itself. Returns how many entries
|
||||
/// moved. Call [`save`](Self::save) afterwards.
|
||||
pub fn move_folder(
|
||||
&mut self,
|
||||
from: impl AsRef<str>,
|
||||
to: impl AsRef<str>,
|
||||
) -> Result<usize, AssetDbError> {
|
||||
let from = checked_relative(from.as_ref())?;
|
||||
let to = checked_relative(to.as_ref())?;
|
||||
if from == to {
|
||||
return Ok(0);
|
||||
}
|
||||
if to.starts_with(&format!("{from}/")) {
|
||||
return Err(AssetDbError::InvalidPath(to));
|
||||
}
|
||||
let assets_dir = self.assets_dir();
|
||||
let from_abs = assets_dir.join(from.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
let to_abs = assets_dir.join(to.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
if !from_abs.is_dir() {
|
||||
return Err(AssetDbError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("no such folder under assets/: {from}"),
|
||||
)));
|
||||
}
|
||||
if to_abs.exists() {
|
||||
return Err(AssetDbError::DestinationExists(to));
|
||||
}
|
||||
if let Some(parent) = to_abs.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::rename(&from_abs, &to_abs)?;
|
||||
|
||||
let prefix = format!("{from}/");
|
||||
let moved: Vec<AssetUid> = self
|
||||
.by_uid
|
||||
.values()
|
||||
.filter(|e| e.path.starts_with(&prefix))
|
||||
.map(|e| e.uid)
|
||||
.collect();
|
||||
for &uid in &moved {
|
||||
let entry = self.by_uid.get_mut(&uid).expect("collected above");
|
||||
let new_path = format!("{to}/{}", &entry.path[prefix.len()..]);
|
||||
let old_path = std::mem::replace(&mut entry.path, new_path.clone());
|
||||
entry.kind = AssetKind::classify(&new_path);
|
||||
self.by_path.remove(&old_path);
|
||||
self.by_path.insert(new_path, uid);
|
||||
}
|
||||
Ok(moved.len())
|
||||
}
|
||||
|
||||
// --- internals ---------------------------------------------------------
|
||||
|
||||
fn highest_uid(&self) -> u64 {
|
||||
@@ -522,6 +666,20 @@ fn normalize_relative(path: &str) -> String {
|
||||
trimmed.trim_start_matches('/').to_string()
|
||||
}
|
||||
|
||||
/// Normalizes like [`normalize_relative`] and rejects paths that are empty or
|
||||
/// escape the assets directory (`..` components) — file operations must never
|
||||
/// touch anything outside `assets/`.
|
||||
fn checked_relative(path: &str) -> Result<String, AssetDbError> {
|
||||
let norm = normalize_relative(path);
|
||||
let escapes = Path::new(&norm)
|
||||
.components()
|
||||
.any(|c| !matches!(c, std::path::Component::Normal(_)));
|
||||
if norm.is_empty() || escapes {
|
||||
return Err(AssetDbError::InvalidPath(path.to_string()));
|
||||
}
|
||||
Ok(norm)
|
||||
}
|
||||
|
||||
/// Recursively collects files under `dir`, pushing each one's path relative to
|
||||
/// `base` (forward slashes) into `out`. A missing `dir` is silently skipped.
|
||||
fn collect_files(dir: &Path, base: &Path, out: &mut Vec<String>) {
|
||||
@@ -738,6 +896,128 @@ mod tests {
|
||||
std::fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_asset_keeps_uid_and_reclassifies() {
|
||||
let root = temp_root("move_asset");
|
||||
touch_asset(&root, "textures/wall.png");
|
||||
let mut db = AssetDatabase::new(&root);
|
||||
db.scan();
|
||||
let uid = db.uid_of("textures/wall.png").unwrap();
|
||||
|
||||
// Rename within the folder: same uid, file moved on disk.
|
||||
db.move_asset(uid, "textures/brick.png").unwrap();
|
||||
assert_eq!(db.relative_path(uid), Some("textures/brick.png"));
|
||||
assert!(db.uid_of("textures/wall.png").is_none());
|
||||
assert!(!root.join(ASSETS_DIR).join("textures/wall.png").exists());
|
||||
assert!(root.join(ASSETS_DIR).join("textures/brick.png").exists());
|
||||
|
||||
// Move into a subfolder that does not exist yet (created), then out of
|
||||
// the typed folder entirely — kind follows the path.
|
||||
db.move_asset(uid, "textures/env/brick.png").unwrap();
|
||||
assert!(root
|
||||
.join(ASSETS_DIR)
|
||||
.join("textures/env/brick.png")
|
||||
.exists());
|
||||
assert_eq!(db.entry(uid).unwrap().kind, AssetKind::Texture);
|
||||
db.move_asset(uid, "misc/brick.dat").unwrap();
|
||||
assert_eq!(db.entry(uid).unwrap().kind, AssetKind::Other);
|
||||
|
||||
// A rescan does not disturb the moved entry's uid... (misc/ is not a
|
||||
// typed folder, so the entry survives only because scan never saw it —
|
||||
// move back first to prove the typed-folder case.)
|
||||
db.move_asset(uid, "textures/brick.png").unwrap();
|
||||
db.scan();
|
||||
assert_eq!(db.uid_of("textures/brick.png"), Some(uid));
|
||||
|
||||
// No-op and error cases.
|
||||
db.move_asset(uid, "textures/brick.png").unwrap(); // unchanged path: Ok
|
||||
touch_asset(&root, "textures/taken.png");
|
||||
db.scan();
|
||||
assert!(matches!(
|
||||
db.move_asset(uid, "textures/taken.png"),
|
||||
Err(AssetDbError::DestinationExists(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
db.move_asset(uid, "../outside.png"),
|
||||
Err(AssetDbError::InvalidPath(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
db.move_asset(AssetUid(999), "textures/x.png"),
|
||||
Err(AssetDbError::UnknownUid(_))
|
||||
));
|
||||
|
||||
std::fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_asset_removes_file_and_entry_without_reusing_uid() {
|
||||
let root = temp_root("delete_asset");
|
||||
touch_asset(&root, "audio/click.wav");
|
||||
let mut db = AssetDatabase::new(&root);
|
||||
db.scan();
|
||||
let uid = db.uid_of("audio/click.wav").unwrap();
|
||||
|
||||
db.delete_asset(uid).unwrap();
|
||||
assert!(db.entry(uid).is_none());
|
||||
assert!(!root.join(ASSETS_DIR).join("audio/click.wav").exists());
|
||||
|
||||
// The freed uid is not handed to the next registration.
|
||||
let next = db.register("audio/other.wav");
|
||||
assert_ne!(next, uid);
|
||||
|
||||
// Deleting an entry whose file is already gone still drops the entry.
|
||||
std::fs::remove_file(root.join(ASSETS_DIR).join("audio/other.wav")).ok();
|
||||
db.delete_asset(next).unwrap();
|
||||
assert!(db.entry(next).is_none());
|
||||
|
||||
assert!(matches!(
|
||||
db.delete_asset(uid),
|
||||
Err(AssetDbError::UnknownUid(_))
|
||||
));
|
||||
|
||||
std::fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_folder_rewrites_entries_and_guards_self_moves() {
|
||||
let root = temp_root("move_folder");
|
||||
touch_asset(&root, "textures/env/wall.png");
|
||||
touch_asset(&root, "textures/env/floor.png");
|
||||
touch_asset(&root, "textures/hero.png");
|
||||
let mut db = AssetDatabase::new(&root);
|
||||
db.scan();
|
||||
let wall = db.uid_of("textures/env/wall.png").unwrap();
|
||||
let hero = db.uid_of("textures/hero.png").unwrap();
|
||||
|
||||
let moved = db.move_folder("textures/env", "textures/world").unwrap();
|
||||
assert_eq!(moved, 2);
|
||||
assert_eq!(db.relative_path(wall), Some("textures/world/wall.png"));
|
||||
assert!(db.uid_of("textures/world/floor.png").is_some());
|
||||
assert_eq!(db.relative_path(hero), Some("textures/hero.png"));
|
||||
assert!(root
|
||||
.join(ASSETS_DIR)
|
||||
.join("textures/world/wall.png")
|
||||
.exists());
|
||||
assert!(!root.join(ASSETS_DIR).join("textures/env").exists());
|
||||
|
||||
// Guards: into itself, onto an existing folder, missing source.
|
||||
assert!(matches!(
|
||||
db.move_folder("textures", "textures/world/deeper"),
|
||||
Err(AssetDbError::InvalidPath(_))
|
||||
));
|
||||
std::fs::create_dir_all(root.join(ASSETS_DIR).join("models")).unwrap();
|
||||
assert!(matches!(
|
||||
db.move_folder("textures/world", "models"),
|
||||
Err(AssetDbError::DestinationExists(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
db.move_folder("textures/nope", "textures/x"),
|
||||
Err(AssetDbError::Io(_))
|
||||
));
|
||||
|
||||
std::fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
struct TxtLoader;
|
||||
impl crate::asset::AssetLoader for TxtLoader {
|
||||
type Asset = String;
|
||||
|
||||
@@ -18,7 +18,8 @@ mod handle;
|
||||
mod server;
|
||||
|
||||
pub use database::{
|
||||
asset_ref_target, AssetDatabase, AssetEntry, AssetKind, AssetRef, AssetUid, ASSET_MANIFEST_FILE,
|
||||
asset_ref_target, AssetDatabase, AssetDbError, AssetEntry, AssetKind, AssetRef, AssetUid,
|
||||
ASSET_MANIFEST_FILE,
|
||||
};
|
||||
pub use gltf::{load_gltf, load_gltf_slice, GltfError, GltfLoader, GltfMesh, GltfModel};
|
||||
pub use handle::{AssetId, Handle, LoadState};
|
||||
|
||||
Reference in New Issue
Block a user