Skip to content

Commit ffcb875

Browse files
authored
Improved the resources loading for balancebot (#9)
1 parent 4ab1a45 commit ffcb875

2 files changed

Lines changed: 163 additions & 96 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
**/logs/*.copper
55
**/logs/*.log
66
**/copper-crash-**.txt
7+
**/.download-cache

examples/cu_rp_balancebot/src/world/mod.rs

Lines changed: 162 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,20 @@ use cu_bevymon::{CuBevyMonFocus, CuBevyMonSurface};
2626
#[cfg(not(target_arch = "wasm32"))]
2727
use std::path::{Path, PathBuf};
2828
#[cfg(not(target_arch = "wasm32"))]
29-
use std::{fs, io};
29+
use std::{
30+
fs,
31+
io::{self, Write},
32+
};
3033

3134
pub const BALANCEBOT: &str = "balancebot.glb";
3235
pub const SKYBOX: &str = "skybox.ktx2";
3336
pub const DIFFUSE_MAP: &str = "diffuse_map.ktx2";
37+
#[cfg(not(target_arch = "wasm32"))]
38+
const BASE_ASSETS_URL: &str = "https://cdn.copper-robotics.com/";
39+
#[cfg(not(target_arch = "wasm32"))]
40+
const SCENE_ASSETS: [&str; 3] = [BALANCEBOT, SKYBOX, DIFFUSE_MAP];
41+
#[cfg(not(target_arch = "wasm32"))]
42+
const SCENE_ASSET_CACHE_DIR: &str = ".download-cache";
3443

3544
const TABLE_HEIGHT: f32 = 0.724;
3645
const RAIL_WIDTH: f32 = 0.55; // 55cm
@@ -103,6 +112,13 @@ struct WorldLayout {
103112
split_monitor: bool,
104113
}
105114

115+
#[derive(Resource, Clone)]
116+
struct SceneAssetPaths {
117+
balance_bot: String,
118+
skybox: String,
119+
diffuse_map: String,
120+
}
121+
106122
#[derive(SystemParam)]
107123
struct SimInput<'w> {
108124
layout: Res<'w, WorldLayout>,
@@ -201,6 +217,7 @@ pub fn build_world(app: &mut App, headless: bool, split_monitor: bool) -> &mut A
201217
.init_resource::<SceneLoadState>()
202218
.insert_resource(Gravity::default())
203219
.insert_resource(Time::<Physics>::default())
220+
.insert_resource(prepare_scene_assets())
204221
.add_systems(Startup, setup_scene)
205222
.add_systems(Startup, setup_ui)
206223
.add_systems(Update, setup_entities) // Wait for the cart entity to be loaded
@@ -292,142 +309,191 @@ fn ground_setup(
292309
}
293310

294311
#[cfg(not(target_arch = "wasm32"))]
295-
fn create_symlink(src: &str, dst: &str) -> io::Result<()> {
296-
let dst_path = Path::new(dst);
312+
fn scene_asset_root() -> PathBuf {
313+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets")
314+
}
297315

298-
if dst_path.exists() {
299-
fs::remove_file(dst_path)?;
316+
#[cfg(not(target_arch = "wasm32"))]
317+
fn scene_asset_cache_root() -> PathBuf {
318+
scene_asset_root().join(SCENE_ASSET_CACHE_DIR)
319+
}
320+
321+
#[cfg(not(target_arch = "wasm32"))]
322+
fn path_string(path: &Path) -> String {
323+
path.to_string_lossy().into_owned()
324+
}
325+
326+
#[cfg(not(target_arch = "wasm32"))]
327+
fn asset_progress_bar(done: usize, total: usize) -> String {
328+
const WIDTH: usize = 28;
329+
let filled = WIDTH * done / total.max(1);
330+
let empty = WIDTH.saturating_sub(filled);
331+
format!(
332+
"[{}{}] {done}/{total}",
333+
"=".repeat(filled),
334+
" ".repeat(empty)
335+
)
336+
}
337+
338+
#[cfg(not(target_arch = "wasm32"))]
339+
fn link_or_copy_cached_asset(src: &Path, dst: &Path) -> io::Result<()> {
340+
if fs::symlink_metadata(dst).is_ok() {
341+
fs::remove_file(dst)?;
300342
}
301343

302344
#[cfg(unix)]
303345
{
304-
std::os::unix::fs::symlink(src, dst)
346+
match std::os::unix::fs::symlink(src, dst) {
347+
Ok(()) => Ok(()),
348+
Err(symlink_err) => fs::copy(src, dst).map(|_| ()).map_err(|copy_err| {
349+
io::Error::new(
350+
copy_err.kind(),
351+
format!("failed to symlink ({symlink_err}) or copy ({copy_err})"),
352+
)
353+
}),
354+
}
305355
}
306356

307357
#[cfg(windows)]
308358
{
309-
std::os::windows::fs::symlink_file(src, dst)
359+
match std::os::windows::fs::symlink_file(src, dst) {
360+
Ok(()) => Ok(()),
361+
Err(symlink_err) => fs::copy(src, dst).map(|_| ()).map_err(|copy_err| {
362+
io::Error::new(
363+
copy_err.kind(),
364+
format!("failed to symlink ({symlink_err}) or copy ({copy_err})"),
365+
)
366+
}),
367+
}
310368
}
311369
}
312370

313-
/// Tries to get the asset path using the online cache first.
314-
/// If that fails due to a network error, falls back to the offline cache.
315371
#[cfg(not(target_arch = "wasm32"))]
316372
fn get_asset_path(
317373
online_cache: &Cache,
318374
offline_cache: &Cache,
319375
asset_url: &str,
320-
asset_name: &str, // For logging purposes
376+
asset_name: &str,
321377
) -> Result<PathBuf, CacheError> {
322-
match online_cache.cached_path(asset_url) {
378+
match offline_cache.cached_path(asset_url) {
323379
Ok(path) => Ok(path),
324380
Err(err) => {
325-
// Check if the error is network-related
326381
if matches!(
327382
err,
328-
CacheError::HttpError(_) | CacheError::IoError(_) | CacheError::ResourceNotFound(_)
383+
CacheError::NoCachedVersions(_) | CacheError::CacheCorrupted(_)
329384
) {
330-
warn!(
331-
"Failed to fetch latest '{}' from network ({}). Attempting to use cached version.",
332-
asset_name, err
333-
);
334-
// Fallback to offline cache
335-
offline_cache.cached_path(asset_url)
385+
eprintln!(" {asset_name}: cache miss; downloading from {asset_url}");
386+
online_cache.cached_path(asset_url)
336387
} else {
337-
// Not a network error, propagate the original error
338388
Err(err)
339389
}
340390
}
341391
}
342392
}
343393

344394
#[cfg(not(target_arch = "wasm32"))]
345-
pub const BASE_ASSETS_URL: &str = "https://cdn.copper-robotics.com/";
395+
fn precached_asset_path(
396+
online_cache: &Cache,
397+
offline_cache: &Cache,
398+
asset_root: &Path,
399+
index: usize,
400+
total: usize,
401+
asset_name: &str,
402+
) -> Result<String, CacheError> {
403+
let plain_path = asset_root.join(asset_name);
404+
if plain_path.is_file() {
405+
eprintln!(
406+
" {} {asset_name}: cached",
407+
asset_progress_bar(index, total)
408+
);
409+
return Ok(path_string(&plain_path));
410+
}
411+
412+
if fs::symlink_metadata(&plain_path).is_ok() {
413+
fs::remove_file(&plain_path)?;
414+
}
415+
416+
eprintln!(
417+
" {} {asset_name}: resolving",
418+
asset_progress_bar(index.saturating_sub(1), total)
419+
);
420+
let asset_url = format!("{BASE_ASSETS_URL}{asset_name}");
421+
let hashed_path = get_asset_path(online_cache, offline_cache, &asset_url, asset_name)?;
422+
link_or_copy_cached_asset(&hashed_path, &plain_path)?;
423+
eprintln!(" {} {asset_name}: ready", asset_progress_bar(index, total));
424+
Ok(path_string(&plain_path))
425+
}
426+
427+
#[cfg(not(target_arch = "wasm32"))]
428+
fn prepare_scene_assets() -> SceneAssetPaths {
429+
let asset_root = scene_asset_root();
430+
let cache_root = scene_asset_cache_root();
431+
fs::create_dir_all(&asset_root).expect("failed to create scene asset directory");
432+
433+
eprintln!(
434+
"Preparing Copper balancebot scene assets in {}",
435+
asset_root.display()
436+
);
437+
let _ = io::stderr().flush();
346438

347-
fn setup_scene(
348-
mut commands: Commands,
349-
asset_server: Res<AssetServer>,
350-
mut images: ResMut<Assets<Image>>,
351-
mut meshes: ResMut<Assets<Mesh>>,
352-
mut materials: ResMut<Assets<StandardMaterial>>,
353-
layout: Res<WorldLayout>,
354-
) {
355-
#[cfg(not(target_arch = "wasm32"))]
356439
let online_cache = Cache::builder()
440+
.dir(cache_root.clone())
357441
.progress_bar(Some(ProgressBar::Full))
358442
.build()
359-
.expect("Failed to create the online file cache.");
360-
361-
#[cfg(not(target_arch = "wasm32"))]
443+
.expect("failed to create online scene asset cache");
362444
let offline_cache = Cache::builder()
363-
.progress_bar(Some(ProgressBar::Full))
445+
.dir(cache_root)
364446
.offline(true)
447+
.progress_bar(None)
365448
.build()
366-
.expect("Failed to create the offline file cache.");
367-
368-
#[cfg(not(target_arch = "wasm32"))]
369-
let balance_bot_url = format!("{BASE_ASSETS_URL}{BALANCEBOT}");
370-
371-
#[cfg(not(target_arch = "wasm32"))]
372-
let balance_bot_hashed =
373-
get_asset_path(&online_cache, &offline_cache, &balance_bot_url, BALANCEBOT)
374-
.expect("Failed to get balancebot.glb (online or cached).");
375-
376-
#[cfg(not(target_arch = "wasm32"))]
377-
let balance_bot_path = balance_bot_hashed.parent().unwrap().join(BALANCEBOT);
378-
379-
#[cfg(not(target_arch = "wasm32"))]
380-
create_symlink(
381-
balance_bot_hashed.to_str().unwrap(),
382-
balance_bot_path.to_str().unwrap(),
383-
)
384-
.expect("Failed to create symlink to balancebot.glb.");
385-
386-
#[cfg(not(target_arch = "wasm32"))]
387-
let skybox_url = format!("{BASE_ASSETS_URL}{SKYBOX}");
388-
389-
#[cfg(not(target_arch = "wasm32"))]
390-
let skybox_path_hashed = get_asset_path(&online_cache, &offline_cache, &skybox_url, SKYBOX)
391-
.expect("Failed to get skybox.ktx2 (online or cached).");
392-
393-
#[cfg(not(target_arch = "wasm32"))]
394-
let skybox_path = skybox_path_hashed.parent().unwrap().join(SKYBOX);
395-
396-
#[cfg(not(target_arch = "wasm32"))]
397-
create_symlink(
398-
skybox_path_hashed.to_str().unwrap(),
399-
skybox_path.to_str().unwrap(),
400-
)
401-
.expect("Failed to create symlink to skybox.ktx2.");
402-
403-
#[cfg(not(target_arch = "wasm32"))]
404-
let diffuse_map_url = format!("{BASE_ASSETS_URL}{DIFFUSE_MAP}");
449+
.expect("failed to create offline scene asset cache");
450+
451+
let total = SCENE_ASSETS.len();
452+
let mut paths = Vec::with_capacity(total);
453+
for (i, asset_name) in SCENE_ASSETS.iter().enumerate() {
454+
paths.push(
455+
precached_asset_path(
456+
&online_cache,
457+
&offline_cache,
458+
&asset_root,
459+
i + 1,
460+
total,
461+
asset_name,
462+
)
463+
.unwrap_or_else(|err| panic!("failed to prepare {asset_name}: {err}")),
464+
);
465+
}
405466

406-
#[cfg(not(target_arch = "wasm32"))]
407-
let diffuse_map_path_hashed =
408-
get_asset_path(&online_cache, &offline_cache, &diffuse_map_url, DIFFUSE_MAP)
409-
.expect("Failed to get diffuse_map.ktx2 (online or cached).");
467+
eprintln!("Scene assets ready.");
468+
SceneAssetPaths {
469+
balance_bot: paths[0].clone(),
470+
skybox: paths[1].clone(),
471+
diffuse_map: paths[2].clone(),
472+
}
473+
}
410474

411-
#[cfg(not(target_arch = "wasm32"))]
412-
let diffuse_map_path = diffuse_map_path_hashed.parent().unwrap().join(DIFFUSE_MAP);
475+
#[cfg(target_arch = "wasm32")]
476+
fn prepare_scene_assets() -> SceneAssetPaths {
477+
SceneAssetPaths {
478+
balance_bot: BALANCEBOT.to_string(),
479+
skybox: SKYBOX.to_string(),
480+
diffuse_map: DIFFUSE_MAP.to_string(),
481+
}
482+
}
413483

414-
#[cfg(not(target_arch = "wasm32"))]
415-
create_symlink(
416-
diffuse_map_path_hashed.to_str().unwrap(),
417-
diffuse_map_path.to_str().unwrap(),
418-
)
419-
.expect("Failed to create symlink to diffuse_map.ktx2.");
420-
421-
#[cfg(target_arch = "wasm32")]
422-
let balance_bot_path = BALANCEBOT;
423-
#[cfg(target_arch = "wasm32")]
424-
let skybox_path = SKYBOX;
425-
#[cfg(target_arch = "wasm32")]
426-
let diffuse_map_path = DIFFUSE_MAP;
427-
428-
let scene_handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset(balance_bot_path));
429-
let skybox_handle = asset_server.load(skybox_path);
430-
let diffuse_map_handle = asset_server.load(diffuse_map_path);
484+
fn setup_scene(
485+
mut commands: Commands,
486+
asset_server: Res<AssetServer>,
487+
mut images: ResMut<Assets<Image>>,
488+
mut meshes: ResMut<Assets<Mesh>>,
489+
mut materials: ResMut<Assets<StandardMaterial>>,
490+
layout: Res<WorldLayout>,
491+
asset_paths: Res<SceneAssetPaths>,
492+
) {
493+
let scene_handle =
494+
asset_server.load(GltfAssetLabel::Scene(0).from_asset(asset_paths.balance_bot.clone()));
495+
let skybox_handle = asset_server.load(asset_paths.skybox.clone());
496+
let diffuse_map_handle = asset_server.load(asset_paths.diffuse_map.clone());
431497
let specular_map_handle = skybox_handle.clone();
432498

433499
commands.insert_resource(GlobalAmbientLight {

0 commit comments

Comments
 (0)