From 96171d9854d4613494223c2a10b08a4ec302b66d Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Fri, 17 Apr 2026 10:08:24 -0300 Subject: [PATCH 01/11] Don't limit render distance in preview mode --- src/lib/actual_web.rs | 12 ++---------- src/main.rs | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/lib/actual_web.rs b/src/lib/actual_web.rs index 80565d0c..de7d9814 100644 --- a/src/lib/actual_web.rs +++ b/src/lib/actual_web.rs @@ -256,16 +256,8 @@ fn main_inner( app.insert_resource(SceneParams::from_query_string(params, true)); app.insert_resource(SceneLoadDistance { - load: if is_preview { - 1.0 - } else { - final_config.scene_load_distance - }, - unload: if is_preview { - 0.0 - } else { - final_config.scene_unload_extra_distance - }, + load: final_config.scene_load_distance, + unload: final_config.scene_unload_extra_distance, load_imposter: final_config .scene_imposter_distances .last() diff --git a/src/main.rs b/src/main.rs index b12417d9..946eb67c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -449,16 +449,8 @@ fn main() { }); app.insert_resource(SceneLoadDistance { - load: if is_preview { - 1.0 - } else { - final_config.scene_load_distance - }, - unload: if is_preview { - 0.0 - } else { - final_config.scene_unload_extra_distance - }, + load: final_config.scene_load_distance, + unload: final_config.scene_unload_extra_distance, load_imposter: final_config .scene_imposter_distances .last() From 713171574f4901774b710a034d29009b98550a7f Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Mon, 20 Apr 2026 09:43:34 -0300 Subject: [PATCH 02/11] Revert "Don't limit render distance in preview mode" This reverts commit 96171d9854d4613494223c2a10b08a4ec302b66d. --- src/lib/actual_web.rs | 12 ++++++++++-- src/main.rs | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/lib/actual_web.rs b/src/lib/actual_web.rs index de7d9814..80565d0c 100644 --- a/src/lib/actual_web.rs +++ b/src/lib/actual_web.rs @@ -256,8 +256,16 @@ fn main_inner( app.insert_resource(SceneParams::from_query_string(params, true)); app.insert_resource(SceneLoadDistance { - load: final_config.scene_load_distance, - unload: final_config.scene_unload_extra_distance, + load: if is_preview { + 1.0 + } else { + final_config.scene_load_distance + }, + unload: if is_preview { + 0.0 + } else { + final_config.scene_unload_extra_distance + }, load_imposter: final_config .scene_imposter_distances .last() diff --git a/src/main.rs b/src/main.rs index 946eb67c..b12417d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -449,8 +449,16 @@ fn main() { }); app.insert_resource(SceneLoadDistance { - load: final_config.scene_load_distance, - unload: final_config.scene_unload_extra_distance, + load: if is_preview { + 1.0 + } else { + final_config.scene_load_distance + }, + unload: if is_preview { + 0.0 + } else { + final_config.scene_unload_extra_distance + }, load_imposter: final_config .scene_imposter_distances .last() From 19bdec578ed3f114efa6e7082f955b1041025120 Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Tue, 21 Apr 2026 08:32:40 -0300 Subject: [PATCH 03/11] Allow scenes to continue rendering in preview mode when outside its bounds --- crates/scene_runner/src/initialize_scene.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/scene_runner/src/initialize_scene.rs b/crates/scene_runner/src/initialize_scene.rs index f9d73430..d244fdff 100644 --- a/crates/scene_runner/src/initialize_scene.rs +++ b/crates/scene_runner/src/initialize_scene.rs @@ -1381,6 +1381,7 @@ pub fn process_scene_lifecycle( mut spawn: EventWriter, pointers: Res, imposter_scene: Res, + preview_mode: Res, ) { let mut required_scene_ids: HashMap<(String, Option), bool> = HashMap::new(); @@ -1393,6 +1394,9 @@ pub fn process_scene_lifecycle( .first() .and_then(|(p, _)| pointers.get(p)) .and_then(PointerResult::hash_and_urn); + if preview_mode.is_preview && current_scene.is_none() { + return; + } let pir = parcels_in_range( focus, From 585de47efc45813718ce9b420afd34872e1b894a Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Wed, 22 Apr 2026 10:57:30 -0300 Subject: [PATCH 04/11] Add filed to `PreviewMode` that holds the location passed to the application --- crates/common/src/structs.rs | 3 ++- src/lib/actual_web.rs | 9 ++++++--- src/main.rs | 13 ++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/common/src/structs.rs b/crates/common/src/structs.rs index 92dfef05..4fe90fbf 100644 --- a/crates/common/src/structs.rs +++ b/crates/common/src/structs.rs @@ -1137,10 +1137,11 @@ pub struct MicState { pub enabled: bool, } -#[derive(Resource, Default)] +#[derive(Debug, Resource, Default)] pub struct PreviewMode { pub server: Option, pub is_preview: bool, + pub preview_parcel: Option, } // resource into which systems can add debug info diff --git a/src/lib/actual_web.rs b/src/lib/actual_web.rs index 80565d0c..704818b2 100644 --- a/src/lib/actual_web.rs +++ b/src/lib/actual_web.rs @@ -88,11 +88,13 @@ fn main_inner( }); let base_graphics = base_config.graphics.clone(); + let location = IVec2Arg::from_str(location) + .map(|l| l.0) + .unwrap_or(base_config.location); + let final_config = AppConfig { server: server.to_owned(), - location: IVec2Arg::from_str(location) - .map(|l| l.0) - .unwrap_or(base_config.location), + location, graphics: common::structs::GraphicsSettings { gpu_bytes_per_frame: rabpf, ..base_graphics @@ -252,6 +254,7 @@ fn main_inner( app.insert_resource(PreviewMode { server: is_preview.then_some(map_realm_name(&final_config.server)), is_preview, + preview_parcel: is_preview.then_some(location), }); app.insert_resource(SceneParams::from_query_string(params, true)); diff --git a/src/main.rs b/src/main.rs index b12417d9..d1c55557 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,16 +149,18 @@ fn main() { Default::default() }); + let location = args + .value_from_str::<_, IVec2Arg>("--location") + .ok() + .map(|va| va.0) + .unwrap_or(base_config.location); + let final_config = AppConfig { server: args .value_from_str("--server") .ok() .unwrap_or(base_config.server), - location: args - .value_from_str::<_, IVec2Arg>("--location") - .ok() - .map(|va| va.0) - .unwrap_or(base_config.location), + location, previous_login: base_config.previous_login, graphics: GraphicsSettings { vsync: args @@ -446,6 +448,7 @@ fn main() { app.insert_resource(PreviewMode { server: is_preview.then_some(map_realm_name(&final_config.server)), is_preview, + preview_parcel: is_preview.then_some(location), }); app.insert_resource(SceneLoadDistance { From c76e233f89a48818f5efec08ae8825131402201b Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Wed, 22 Apr 2026 12:13:34 -0300 Subject: [PATCH 05/11] Add `parcel_to_vec3` --- crates/scene_runner/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/scene_runner/src/lib.rs b/crates/scene_runner/src/lib.rs index 1275ecc6..e522326d 100644 --- a/crates/scene_runner/src/lib.rs +++ b/crates/scene_runner/src/lib.rs @@ -501,6 +501,14 @@ pub fn vec3_to_parcel(position: Vec3) -> IVec2 { .as_ivec2() } +pub fn parcel_to_vec3(parcel: IVec2) -> Vec3 { + Vec3::new( + (parcel.x as f32 + 0.5) * PARCEL_SIZE, + 0., + -(parcel.y as f32 + 0.5) * PARCEL_SIZE, + ) +} + impl ContainingScene<'_, '_> { // just the parcel at the position pub fn get_parcel_position(&self, position: Vec3) -> Option { From 68ee8287c98a91bee672795d728c391ca77d018b Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Wed, 22 Apr 2026 12:25:47 -0300 Subject: [PATCH 06/11] Maintain initial scene loaded when in preview --- crates/scene_runner/src/initialize_scene.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/scene_runner/src/initialize_scene.rs b/crates/scene_runner/src/initialize_scene.rs index d244fdff..7fdd9bf2 100644 --- a/crates/scene_runner/src/initialize_scene.rs +++ b/crates/scene_runner/src/initialize_scene.rs @@ -43,6 +43,7 @@ use system_bridge::{LiveSceneInfo, SystemApi, SystemBridge}; use super::{update_world::CrdtExtractors, LoadSceneEvent, PrimaryUser, SceneSets, SceneUpdates}; use crate::{ bounds_calc::scene_regions, + parcel_to_vec3, renderer_context::RendererSceneContext, update_world::{visibility::VisibilityComponent, ComponentTracker}, vec3_to_parcel, ContainerEntity, DeletedSceneEntities, OutOfWorld, SceneEntity, @@ -1366,7 +1367,7 @@ fn load_active_entities( #[derive(Resource, Default)] pub struct CurrentImposterScene(pub Option<(PointerResult, bool)>); -#[allow(clippy::type_complexity, clippy::too_many_arguments)] +#[expect(clippy::type_complexity, clippy::too_many_arguments)] pub fn process_scene_lifecycle( mut commands: Commands, current_realm: Res, @@ -1389,17 +1390,19 @@ pub fn process_scene_lifecycle( let Ok(focus) = focus.single() else { return; }; + let focus = preview_mode + .preview_parcel + .as_ref() + .map(|p| GlobalTransform::from(Transform::from_translation(parcel_to_vec3(*p)))) + .unwrap_or(*focus); - let current_scene = parcels_in_range(focus, 0.0, pointers.min(), pointers.max()) + let current_scene = parcels_in_range(&focus, 0.0, pointers.min(), pointers.max()) .first() .and_then(|(p, _)| pointers.get(p)) .and_then(PointerResult::hash_and_urn); - if preview_mode.is_preview && current_scene.is_none() { - return; - } let pir = parcels_in_range( - focus, + &focus, range.load + range.unload, pointers.min(), pointers.max(), From b998d2e507f037acafcab29984ee4964bc8ab9b2 Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Tue, 28 Apr 2026 09:58:11 -0300 Subject: [PATCH 07/11] Add `lock_preview` and `unlock_preview` --- src/lib/actual_web.rs | 48 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 52 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/lib/actual_web.rs b/src/lib/actual_web.rs index 704818b2..9dd74299 100644 --- a/src/lib/actual_web.rs +++ b/src/lib/actual_web.rs @@ -335,6 +335,8 @@ fn main_inner( app.add_console_command::(scene_distance); app.add_console_command::(scene_threads); app.add_console_command::(set_fps); + app.add_console_command::(lock_preview); + app.add_console_command::(unlock_preview); app.add_systems( Update, @@ -461,6 +463,52 @@ fn scene_distance( } } +/// Locks the preview mode to the current parcel +#[derive(clap::Parser, ConsoleCommand)] +#[command(name = "/lock_preview")] +struct LockPreviewCommand; + +fn lock_preview( + mut input: ConsoleCommand, + mut preview_mode: ResMut, + focus: Single<&GlobalTransform, With>, + pointers: Res, +) { + if let Some(Ok(_command)) = input.take() { + let Some((parcel, _)) = parcels_in_range(&focus, 0.0, pointers.min(), pointers.max()).pop() + else { + unreachable!("Player should never be in a invalid parcel."); + }; + let Some(_current_scene) = pointers.get(parcel) else { + input.reply_failed(format!("failed to locked preview to parcel {}", parcel)); + return; + }; + preview_mode.preview_parcel = Some(parcel); + + input.reply_ok(format!("locked preview to parcel {}", parcel)); + } +} + +/// Unlocks the preview mode to the current parcel +#[derive(clap::Parser, ConsoleCommand)] +#[command(name = "/unlock_preview")] +struct UnlockPreviewCommand; + +fn unlock_preview( + mut input: ConsoleCommand, + mut preview_mode: ResMut, +) { + if let Some(Ok(_command)) = input.take() { + let parcel = preview_mode.preview_parcel.take(); + + if let Some(parcel) = parcel { + input.reply_ok(format!("unlocked preview to parcel {}", parcel)); + } else { + input.reply("Preview was not locked to a parcel."); + } + } +} + // set thread count #[derive(clap::Parser, ConsoleCommand)] #[command(name = "/scene_threads")] diff --git a/src/main.rs b/src/main.rs index 39975a9c..699808e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ use restricted_actions::{process_startup_scenes, RestrictedActionsPlugin}; use scene_material::SceneBoundPlugin; use scene_runner::{ automatic_testing::AutomaticTestingPlugin, - initialize_scene::{TestingData, PARCEL_SIZE}, + initialize_scene::{parcels_in_range, ScenePointers, TestingData, PARCEL_SIZE}, update_world::NoGltf, OutOfWorld, SceneRunnerPlugin, }; @@ -448,7 +448,7 @@ fn main() { app.insert_resource(PreviewMode { server: is_preview.then_some(map_realm_name(&final_config.server)), is_preview, - preview_parcel: is_preview.then_some(location), + preview_parcel: None, }); app.insert_resource(SceneLoadDistance { @@ -542,6 +542,8 @@ fn main() { app.add_console_command::(scene_distance); app.add_console_command::(scene_threads); app.add_console_command::(set_fps); + app.add_console_command::(lock_preview); + app.add_console_command::(unlock_preview); info!("Bevy-Explorer version {}", version); @@ -667,6 +669,52 @@ fn scene_distance( } } +/// Locks the preview mode to the current parcel +#[derive(clap::Parser, ConsoleCommand)] +#[command(name = "/lock_preview")] +struct LockPreviewCommand; + +fn lock_preview( + mut input: ConsoleCommand, + mut preview_mode: ResMut, + focus: Single<&GlobalTransform, With>, + pointers: Res, +) { + if let Some(Ok(_command)) = input.take() { + let Some((parcel, _)) = parcels_in_range(&focus, 0.0, pointers.min(), pointers.max()).pop() + else { + unreachable!("Player should never be in a invalid parcel."); + }; + let Some(_current_scene) = pointers.get(parcel) else { + input.reply_failed(format!("failed to locked preview to parcel {}", parcel)); + return; + }; + preview_mode.preview_parcel = Some(parcel); + + input.reply_ok(format!("locked preview to parcel {}", parcel)); + } +} + +/// Unlocks the preview mode to the current parcel +#[derive(clap::Parser, ConsoleCommand)] +#[command(name = "/unlock_preview")] +struct UnlockPreviewCommand; + +fn unlock_preview( + mut input: ConsoleCommand, + mut preview_mode: ResMut, +) { + if let Some(Ok(_command)) = input.take() { + let parcel = preview_mode.preview_parcel.take(); + + if let Some(parcel) = parcel { + input.reply_ok(format!("unlocked preview to parcel {}", parcel)); + } else { + input.reply("Preview was not locked to a parcel."); + } + } +} + // set thread count #[derive(clap::Parser, ConsoleCommand)] #[command(name = "/scene_threads")] From aba42c2c96a9f9ea33b1212e596eaaad22cd9d46 Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Tue, 28 Apr 2026 10:00:44 -0300 Subject: [PATCH 08/11] Unlock preview on wasm startup --- src/lib/actual_web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/actual_web.rs b/src/lib/actual_web.rs index 9dd74299..c1bd645a 100644 --- a/src/lib/actual_web.rs +++ b/src/lib/actual_web.rs @@ -254,7 +254,7 @@ fn main_inner( app.insert_resource(PreviewMode { server: is_preview.then_some(map_realm_name(&final_config.server)), is_preview, - preview_parcel: is_preview.then_some(location), + preview_parcel: None, }); app.insert_resource(SceneParams::from_query_string(params, true)); From 748d7d2f85ffda1bd48d0f25a2ca047ae84961c7 Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Tue, 28 Apr 2026 10:51:56 -0300 Subject: [PATCH 09/11] Fix imports --- src/lib/actual_web.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/actual_web.rs b/src/lib/actual_web.rs index c1bd645a..2b791662 100644 --- a/src/lib/actual_web.rs +++ b/src/lib/actual_web.rs @@ -36,7 +36,10 @@ use image_processing::ImageProcessingPlugin; use imposters::DclImposterPlugin; use restricted_actions::{process_startup_scenes, RestrictedActionsPlugin}; use scene_material::SceneBoundPlugin; -use scene_runner::{initialize_scene::TestingData, vec3_to_parcel, OutOfWorld, SceneRunnerPlugin}; +use scene_runner::{ + initialize_scene::{parcels_in_range, ScenePointers, TestingData}, + vec3_to_parcel, OutOfWorld, SceneRunnerPlugin, +}; use av::AVPlayerPlugin; use avatar::AvatarPlugin; From 01fb9cba00f1e068f14a7e1907717bc174f61e4b Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Tue, 28 Apr 2026 14:47:00 -0300 Subject: [PATCH 10/11] Disable realm change in preview mode --- crates/ipfs/src/lib.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/ipfs/src/lib.rs b/crates/ipfs/src/lib.rs index 4cf40b0e..f5c58ed4 100644 --- a/crates/ipfs/src/lib.rs +++ b/crates/ipfs/src/lib.rs @@ -43,7 +43,7 @@ use bevy::asset::io::wasm::HttpWasmAssetReader; use bevy_console::{ConsoleCommand, PrintConsoleLine}; use common::{ sets::RealmLifecycle, - structs::{AppConfig, CommsConfig, CurrentRealm, ServerConfiguration}, + structs::{AppConfig, CommsConfig, CurrentRealm, PreviewMode, ServerConfiguration}, util::TaskCompat, }; use ipfs_path::IpfsAsset; @@ -574,6 +574,7 @@ pub fn change_realm( >, mut current_realm: ResMut, mut print: EventWriter, + preview_mode: Res, ) { match *realm_change { None => *realm_change = Some(ipfs.realm_config_receiver.clone()), @@ -606,16 +607,23 @@ pub fn change_realm( } if !change_realm_requests.is_empty() { - let ipfs = ipfs.clone(); - let request = change_realm_requests.read().last().unwrap(); - - let new_realm = map_realm_name(&request.new_realm); - let content_server_override = request.content_server_override.to_owned(); - IoTaskPool::get() - .spawn_compat(async move { - ipfs.set_realm(new_realm, content_server_override).await; - }) - .detach(); + if preview_mode.is_preview { + print.write(PrintConsoleLine { + line: "Changing realm is disabled in preview mode.".to_owned(), + }); + change_realm_requests.clear(); + } else { + let ipfs = ipfs.clone(); + let request = change_realm_requests.read().last().unwrap(); + + let new_realm = map_realm_name(&request.new_realm); + let content_server_override = request.content_server_override.to_owned(); + IoTaskPool::get() + .spawn_compat(async move { + ipfs.set_realm(new_realm, content_server_override).await; + }) + .detach(); + } } } From a5aa823596615af04a548d5206a8fcd64c99d2db Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Tue, 28 Apr 2026 15:28:34 -0300 Subject: [PATCH 11/11] Allow system info to show stats from the locked parcel even when out of bounds --- crates/system_ui/src/sysinfo.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/system_ui/src/sysinfo.rs b/crates/system_ui/src/sysinfo.rs index 8954a776..80d4b730 100644 --- a/crates/system_ui/src/sysinfo.rs +++ b/crates/system_ui/src/sysinfo.rs @@ -25,12 +25,13 @@ use console::DoAddConsoleCommand; use scene_material::{SceneMaterial, SCENE_MATERIAL_OUTLINE}; use scene_runner::{ initialize_scene::{SceneLoading, TestingData, PARCEL_SIZE}, + parcel_to_vec3, renderer_context::RendererSceneContext, update_world::{ gltf_container::{GltfLoadingCount, SceneResourceLookup}, ComponentTracker, TrackComponents, }, - ContainerEntity, ContainingScene, Toaster, + vec3_to_parcel, ContainerEntity, ContainingScene, Toaster, }; use ui_core::{ bound_node::BoundedImageMaterial, @@ -441,23 +442,25 @@ fn setup_minimap( fn update_minimap( q: Query<&DuiEntities, With>, mut maps: Query<&mut MapTexture>, - player: Query<(Entity, &GlobalTransform), With>, + player: Query<&GlobalTransform, With>, containing_scene: ContainingScene, scenes: Query<(&RendererSceneContext, Option<&GltfLoadingCount>)>, mut text: Query<&mut Text>, preview: Res, ) { - let Ok((player, gt)) = player.single() else { + let Ok(gt) = player.single() else { return; }; let player_translation = (gt.translation().xz() * Vec2::new(1.0, -1.0)) / PARCEL_SIZE; let map_center = player_translation - Vec2::Y; + let parcel = preview + .preview_parcel + .unwrap_or_else(|| player_translation.floor().as_ivec2()); let scene = containing_scene - .get_parcel_oow(player) + .get_parcel_position(parcel_to_vec3(parcel)) .and_then(|scene| scenes.get(scene).ok()); - let parcel = player_translation.floor().as_ivec2(); let title = scene .map(|(context, _)| context.title.clone()) .unwrap_or("???".to_owned()); @@ -500,7 +503,7 @@ fn update_tracker( mut q: Query<(Ref, &DuiEntities)>, stats: Query<&SceneResourceLookup>, f: Res, - player: Query>, + player: Query<&GlobalTransform, With>, containing_scene: ContainingScene, dui: Res, mesh_handles: Query<(&Mesh3d, &ContainerEntity, &Visibility)>, @@ -514,6 +517,7 @@ fn update_tracker( materials: Res>, diagnostics: Res, images: Res>, + preview: Res, ) { let Ok((tracker, entities)) = q.single_mut() else { return; @@ -535,7 +539,10 @@ fn update_tracker( return; }; - let scenes = containing_scene.get_parcel(player); + let parcel = preview + .preview_parcel + .unwrap_or_else(|| vec3_to_parcel(player.translation())); + let scenes = containing_scene.get_parcel_position(parcel_to_vec3(parcel)); let Some(scene) = scenes.iter().next() else { return; };