//! Movement, object, parcel, estate, and grid command groups for native `TestClient`. #![allow( clippy::elidable_lifetime_names, clippy::format_push_string, clippy::needless_pass_by_value, clippy::too_many_lines, private_interfaces )] use super::{ClientManager, FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock}; use libremetaverse::types::compat::{CancellationToken, Subscription}; use libremetaverse::types::{AssetType, FolderType, PrimFlags, Quaternion, UUID, Vector2, Vector3}; use libremetaverse::{ GridLayer, GridRegion, Parcel, PermissionMask, Primitive, PrimitiveObjectProperties, PrimitiveParticleSystemSourcePattern, PrimitiveTextureEntry, Tree, }; use libremetaverse_structured_data::{OSD, OSDParser}; use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs; use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::Duration; const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024; const MAX_EXPORT_PRIMS: usize = 10_000; const MAX_QUERY_RESULTS: usize = 65_535; const MAX_MOVEMENT_SECONDS: u64 = 60; #[derive(Clone, Copy)] pub(super) enum Command { DownloadTerrain, EstateCovenant, UploadTerrain, AgentLocations, FindSim, GridLayer, GridMap, ParcelDetails, ParcelInfo, PrimOwners, SelectObjects, SyntaxId, Wind, Back, CrossRegion, Crouch, Fly, FlyTo, Follow, Forward, GoHome, Goto, GotoLandmark, Jump, Left, Location, MoveTo, Right, SetHome, Sit, SitOn, Stand, TurnTo, ChangePerms, DeRez, DownloadTexture, Export, ExportParticles, FindObjects, FindTexture, Import, PrimCount, PrimInfo, PrimRegex, Textures, Tree, } pub(super) fn commands() -> Vec { use super::{CommandCategory as Cat, CommandHandler}; use Command as C; let world = |command| CommandHandler::World(command); vec![ ( "downloadterrain", "Download the current estate RAW terrain. Usage: downloadterrain [timeout-ms] [output] --confirm", Cat::Simulator, world(C::DownloadTerrain), ), ( "getestatecovenant", "Retrieve estate covenant information. Usage: getestatecovenant [timeout-seconds]", Cat::Simulator, world(C::EstateCovenant), ), ( "uploadterrain", "Upload a RAW terrain file. Usage: uploadterrain [file] --confirm", Cat::Simulator, world(C::UploadTerrain), ), ( "agentlocations", "List agent map locations. Usage: agentlocations [region-handle]", Cat::Simulator, world(C::AgentLocations), ), ( "findsim", "Find a simulator. Usage: findsim [simulator name]", Cat::Simulator, world(C::FindSim), ), ( "gridlayer", "Download grid object-map layer chunks", Cat::Simulator, world(C::GridLayer), ), ( "gridmap", "Download visible grid map information", Cat::Simulator, world(C::GridMap), ), ( "parceldetails", "Display parcel details. Usage: parceldetails [parcel-id]", Cat::Parcel, world(C::ParcelDetails), ), ( "parcelinfo", "Display all parcels in the current simulator", Cat::Parcel, world(C::ParcelInfo), ), ( "primowners", "Display parcel prim owners. Usage: primowners [parcel-id]", Cat::Parcel, world(C::PrimOwners), ), ( "selectobjects", "List parcel objects for an owner. Usage: selectobjects [parcel-id] [owner-uuid]", Cat::Parcel, world(C::SelectObjects), ), ( "syntaxid", "Display the current native LSL syntax identifiers", Cat::Simulator, world(C::SyntaxId), ), ( "wind", "Display local wind data", Cat::Simulator, world(C::Wind), ), ( "back", "Move backward. Usage: back [seconds] --confirm", Cat::Movement, world(C::Back), ), ( "crossregion", "Cross a region border. Usage: crossregion [direction] [walk|fly] --confirm", Cat::Movement, world(C::CrossRegion), ), ( "crouch", "Start or stop crouching. Usage: crouch [start|stop] --confirm", Cat::Movement, world(C::Crouch), ), ( "fly", "Start or stop flying. Usage: fly [start|stop] --confirm", Cat::Movement, world(C::Fly), ), ( "flyto", "Fly toward a position. Usage: flyto x y z [seconds] --confirm", Cat::Movement, world(C::FlyTo), ), ( "follow", "Follow another avatar. Usage: follow [First Last|off] --confirm", Cat::Movement, world(C::Follow), ), ( "forward", "Move forward. Usage: forward [seconds] --confirm", Cat::Movement, world(C::Forward), ), ( "gohome", "Teleport home. Usage: gohome --confirm", Cat::Movement, world(C::GoHome), ), ( "goto", "Teleport to a location. Usage: goto sim/x/y/z --confirm", Cat::Movement, world(C::Goto), ), ( "goto_landmark", "Teleport to a landmark. Usage: goto_landmark [uuid] --confirm", Cat::Movement, world(C::GotoLandmark), ), ( "jump", "Jump or fly up. Usage: jump --confirm", Cat::Movement, world(C::Jump), ), ( "left", "Move left. Usage: left [seconds] --confirm", Cat::Movement, world(C::Left), ), ( "location", "Show the current simulator and position", Cat::Movement, world(C::Location), ), ( "moveto", "Use simulator autopilot. Usage: moveto x y z --confirm", Cat::Movement, world(C::MoveTo), ), ( "right", "Move right. Usage: right [seconds] --confirm", Cat::Movement, world(C::Right), ), ( "sethome", "Set home to the current location. Usage: sethome --confirm", Cat::Movement, world(C::SetHome), ), ( "sit", "Sit on the closest primitive. Usage: sit --confirm", Cat::Movement, world(C::Sit), ), ( "siton", "Sit on a primitive. Usage: siton [uuid] --confirm", Cat::Movement, world(C::SitOn), ), ( "stand", "Stand up. Usage: stand --confirm", Cat::Movement, world(C::Stand), ), ( "turnto", "Turn toward a point. Usage: turnto x y z --confirm", Cat::Movement, world(C::TurnTo), ), ( "changeperms", "Change linkset next-owner permissions. Usage: changeperms [uuid] [copy] [mod] [xfer] --confirm", Cat::Objects, world(C::ChangePerms), ), ( "derez", "Take a primitive into Trash. Usage: derez [uuid] --confirm", Cat::Objects, world(C::DeRez), ), ( "downloadtexture", "Download a texture. Usage: downloadtexture [uuid] [discard-level] [output]", Cat::Inventory, world(C::DownloadTexture), ), ( "export", "Export a linkset and its textures. Usage: export [uuid] [output.xml]", Cat::Objects, world(C::Export), ), ( "exportparticles", "Convert a particle system to LSL. Usage: exportparticles [uuid]", Cat::Objects, world(C::ExportParticles), ), ( "findobjects", "Find objects by radius and name. Usage: findobjects [radius] [search]", Cat::Objects, world(C::FindObjects), ), ( "findtexture", "Find a texture on a face. Usage: findtexture [face-index] [uuid]", Cat::Objects, world(C::FindTexture), ), ( "import", "Import linksets from XML. Usage: import [input.xml] [usegroup] --confirm", Cat::Objects, world(C::Import), ), ( "primcount", "Show tracked avatar and primitive counts", Cat::TestClient, world(C::PrimCount), ), ( "priminfo", "Display primitive details. Usage: priminfo [uuid]", Cat::Objects, world(C::PrimInfo), ), ( "primregex", "Find primitives by regular-expression-like text. Usage: primregex [predicate]", Cat::Objects, world(C::PrimRegex), ), ( "textures", "Enable or disable automatic texture downloading. Usage: textures [on|off]", Cat::Objects, world(C::Textures), ), ( "tree", "Rez a tree. Usage: tree [species] --confirm", Cat::Objects, world(C::Tree), ), ] } #[derive(Clone, Debug)] struct AvatarView { id: UUID, name: String, position: Vector3, } #[derive(Clone, Debug)] struct OwnerView { owner: UUID, count: i32, } #[derive(Clone, Debug)] struct ParcelView { parcel: Parcel, owners: Vec, selected: HashMap>, } #[derive(Clone, Debug)] struct AgentLocation { count: i32, x: u32, y: u32, } #[derive(Clone, Debug)] struct EstateView { name: String, owner: UUID, covenant: UUID, timestamp: u32, body: String, terrain: Vec, } #[derive(Clone, Debug, Default)] struct Snapshot { region_name: String, region_handle: u64, position: Vector3, wind: Option>, primitives: Vec, avatars: Vec, parcels: Vec, regions: Vec, layers: Vec, locations: HashMap>, estate: Option, assets: HashMap>, syntax: Vec, } pub(super) struct State { policy: super::inventory::MutationPolicy, fake: Snapshot, textures_enabled: bool, requested_textures: HashSet, follow_target: Option, } impl State { pub(super) fn new(policy: super::inventory::MutationPolicy, textures_enabled: bool) -> Self { Self { policy, fake: Snapshot::default(), textures_enabled, requested_textures: HashSet::new(), follow_target: None, } } } #[derive(Clone, Copy, Debug)] enum MoveDirection { Back, Forward, Left, Right, } #[derive(Clone, Debug)] enum Movement { Pulse(MoveDirection, Duration), Crouch(bool), Fly(bool), FlyTo(Vector3, Duration), Follow(Option<(UUID, Vector3)>), GoHome, TeleportRegion(String, Vector3), TeleportLandmark(UUID), Jump, AutoPilot { local: Vector3, global: [f64; 3] }, SetHome, Sit(UUID), Stand, Turn(Vector3), Cross(Vector3, bool), } #[derive(Clone, Debug)] enum Mutation { Movement(Movement), ChangePermissions { root: UUID, permissions: PermissionMask, }, DeRez(UUID), Import { primitives: Vec, use_group: bool, }, SetTextures(bool), Tree(Tree), UploadTerrain { name: String, data: Vec, }, } #[derive(Clone, Debug)] enum Query { AgentLocations(u64), FindRegion(String), GridLayer, GridMap, Parcels, ParcelOwners(i32), ParcelObjects(i32, UUID), EstateCovenant(Duration), DownloadTerrain(Duration), } pub(super) trait Backend { fn world_state(&self) -> &Mutex; fn world_agent_id(&self) -> UUID; fn world_snapshot<'a>( &'a self, cancellation: CancellationToken, ) -> BackendFuture<'a, Result>; fn world_query<'a>( &'a self, query: Query, cancellation: CancellationToken, ) -> BackendFuture<'a, Result>; fn world_mutate<'a>( &'a self, mutation: Mutation, cancellation: CancellationToken, ) -> BackendFuture<'a, Result>; fn world_texture<'a>( &'a self, id: UUID, discard: i32, cancellation: CancellationToken, ) -> BackendFuture<'a, Result, String>>; fn world_resolve_avatar<'a>( &'a self, name: &'a str, cancellation: CancellationToken, ) -> BackendFuture<'a, Result, String>>; fn apply_world_fixture(&self, _fields: &[&str]) -> Result<(), &'static str> { Err("world fixture requires fake-grid mode") } } pub(super) async fn execute( backend: &B, command: Command, args: &[String], cancellation: CancellationToken, ) -> String { let result = match command { Command::DownloadTerrain => download_terrain(backend, args, cancellation).await, Command::EstateCovenant => estate_covenant(backend, args, cancellation).await, Command::UploadTerrain => upload_terrain(backend, args, cancellation).await, Command::AgentLocations => agent_locations(backend, args, cancellation).await, Command::FindSim => find_sim(backend, args, cancellation).await, Command::GridLayer => grid_layer(backend, args, cancellation).await, Command::GridMap => grid_map(backend, args, cancellation).await, Command::ParcelDetails => parcel_details(backend, args, cancellation).await, Command::ParcelInfo => parcel_info(backend, args, cancellation).await, Command::PrimOwners => prim_owners(backend, args, cancellation).await, Command::SelectObjects => select_objects(backend, args, cancellation).await, Command::SyntaxId => syntax_id(backend, args, cancellation).await, Command::Wind => wind(backend, args, cancellation).await, Command::Back => directional(backend, args, MoveDirection::Back, cancellation).await, Command::Forward => directional(backend, args, MoveDirection::Forward, cancellation).await, Command::Left => directional(backend, args, MoveDirection::Left, cancellation).await, Command::Right => directional(backend, args, MoveDirection::Right, cancellation).await, Command::CrossRegion => cross_region(backend, args, cancellation).await, Command::Crouch => toggle_movement(backend, args, true, cancellation).await, Command::Fly => toggle_movement(backend, args, false, cancellation).await, Command::FlyTo => fly_to(backend, args, cancellation).await, Command::Follow => follow(backend, args, cancellation).await, Command::GoHome => { simple_movement( backend, args, Movement::GoHome, "Teleport Home Succesful", cancellation, ) .await } Command::Goto => goto(backend, args, cancellation).await, Command::GotoLandmark => goto_landmark(backend, args, cancellation).await, Command::Jump => { simple_movement(backend, args, Movement::Jump, "Jumped", cancellation).await } Command::Location => location(backend, args, cancellation).await, Command::MoveTo => move_to(backend, args, cancellation).await, Command::SetHome => { simple_movement(backend, args, Movement::SetHome, "Home Set", cancellation).await } Command::Sit => sit(backend, args, None, cancellation).await, Command::SitOn => sit(backend, args, Some(()), cancellation).await, Command::Stand => { simple_movement(backend, args, Movement::Stand, "Standing up.", cancellation).await } Command::TurnTo => turn_to(backend, args, cancellation).await, Command::ChangePerms => change_permissions(backend, args, cancellation).await, Command::DeRez => derez(backend, args, cancellation).await, Command::DownloadTexture => download_texture(backend, args, cancellation).await, Command::Export => export(backend, args, cancellation).await, Command::ExportParticles => export_particles(backend, args, cancellation).await, Command::FindObjects => find_objects(backend, args, cancellation).await, Command::FindTexture => find_texture(backend, args, cancellation).await, Command::Import => import(backend, args, cancellation).await, Command::PrimCount => prim_count(backend, args, cancellation).await, Command::PrimInfo => prim_info(backend, args, cancellation).await, Command::PrimRegex => prim_regex(backend, args, cancellation).await, Command::Textures => textures(backend, args, cancellation).await, Command::Tree => tree(backend, args, cancellation).await, }; result.unwrap_or_else(|error| error) } fn authorize( backend: &B, args: &[String], spending: bool, estate: bool, ) -> Result, String> { let policy = lock(backend.world_state()).policy; if !policy.mutations() { return Err("Live world mutation blocked; restart with --allow-live-mutations".into()); } if spending && !policy.spending() { return Err("Upload blocked; restart with --allow-spending".into()); } if estate && !policy.estate() { return Err("Estate action blocked; restart with --allow-estate-actions".into()); } if !args.iter().any(|value| value == "--confirm") { return Err("Operation requires an explicit --confirm argument".into()); } Ok(args .iter() .filter(|value| value.as_str() != "--confirm") .cloned() .collect()) } fn parse_uuid(value: &str) -> Result { UUID::new_with_string(value.into()).map_err(|_| format!("{value} is not a valid UUID")) } fn parse_vector(args: &[String], usage: &str) -> Result { if args.len() != 3 { return Err(usage.into()); } let values = args .iter() .map(|value| value.parse::()) .collect::, _>>() .map_err(|_| usage.to_owned())?; if !values.iter().all(|value| value.is_finite()) { return Err(usage.into()); } Vector3::new_with_single_single_single(values[0], values[1], values[2]) .map_err(|_| usage.into()) } fn safe_path(value: &str) -> Result { let path = PathBuf::from(value); if path .components() .any(|component| matches!(component, Component::ParentDir)) { return Err("Parent path traversal is not allowed".into()); } Ok(path) } fn read_bounded(path: &Path) -> Result, String> { let metadata = fs::metadata(path) .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?; if metadata.len() > MAX_FILE_BYTES { return Err(format!("{} exceeds the 64 MiB limit", path.display())); } fs::read(path).map_err(|error| format!("Could not read {}: {error}", path.display())) } fn write_bounded(path: &Path, data: &[u8]) -> Result<(), String> { if u64::try_from(data.len()).unwrap_or(u64::MAX) > MAX_FILE_BYTES { return Err("Output exceeds the 64 MiB limit".into()); } if let Some(parent) = path .parent() .filter(|parent| !parent.as_os_str().is_empty()) { fs::create_dir_all(parent) .map_err(|error| format!("Could not create {}: {error}", parent.display()))?; } fs::write(path, data).map_err(|error| format!("Could not write {}: {error}", path.display())) } async fn directional( backend: &B, args: &[String], direction: MoveDirection, cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.len() > 1 { return Err(format!( "Usage: {} [seconds] --confirm", direction_name(direction) )); } let seconds = args .first() .map_or(Ok(0), |value| value.parse::()) .map_err(|_| format!("Usage: {} [seconds] --confirm", direction_name(direction)))?; if seconds > MAX_MOVEMENT_SECONDS { return Err("Movement duration must not exceed 60 seconds".into()); } backend .world_mutate( Mutation::Movement(Movement::Pulse(direction, Duration::from_secs(seconds))), cancellation, ) .await?; Ok(format!("Moved {}", direction_past(direction))) } const fn direction_name(value: MoveDirection) -> &'static str { match value { MoveDirection::Back => "back", MoveDirection::Forward => "forward", MoveDirection::Left => "left", MoveDirection::Right => "right", } } const fn direction_past(value: MoveDirection) -> &'static str { match value { MoveDirection::Back => "backward", MoveDirection::Forward => "forward", MoveDirection::Left => "left", MoveDirection::Right => "right", } } async fn toggle_movement( backend: &B, args: &[String], crouch: bool, cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.len() > 1 { return Err(if crouch { "Usage: crouch [start/stop] --confirm" } else { "Usage: fly [start/stop] --confirm" } .into()); } let enabled = !args .first() .is_some_and(|arg| arg.eq_ignore_ascii_case("stop")); let mutation = if crouch { Movement::Crouch(enabled) } else { Movement::Fly(enabled) }; backend .world_mutate(Mutation::Movement(mutation), cancellation) .await?; Ok(format!( "{} {}", if enabled { "Started" } else { "Stopped" }, if crouch { "crouching" } else { "flying" } )) } async fn simple_movement( backend: &B, args: &[String], movement: Movement, output: &str, cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if !args.is_empty() { return Err("This command accepts only --confirm".into()); } backend .world_mutate(Mutation::Movement(movement), cancellation) .await?; Ok(output.into()) } async fn fly_to( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if !(3..=4).contains(&args.len()) { return Err("Usage: flyto x y z [seconds] --confirm".into()); } let target = parse_vector(&args[..3], "Usage: flyto x y z [seconds] --confirm")?; let seconds = args .get(3) .map_or(Ok(10), |value| value.parse::()) .map_err(|_| "Usage: flyto x y z [seconds] --confirm")?; if seconds == 0 || seconds > MAX_MOVEMENT_SECONDS { return Err("FlyTo duration must be between 1 and 60 seconds".into()); } backend .world_mutate( Mutation::Movement(Movement::FlyTo(target, Duration::from_secs(seconds))), cancellation, ) .await?; Ok(format!("flying to {target:?} in {seconds} seconds")) } async fn cross_region( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.is_empty() || args.len() > 2 { return Err("Usage: crossregion [direction] [walk/fly] --confirm".into()); } let (direction, name) = match args[0].to_ascii_lowercase().as_str() { "n" | "north" => ((0.0, 1.0), "North"), "s" | "south" => ((0.0, -1.0), "South"), "e" | "east" => ((1.0, 0.0), "East"), "w" | "west" => ((-1.0, 0.0), "West"), "ne" | "northeast" => ((1.0, 1.0), "Northeast"), "nw" | "northwest" => ((-1.0, 1.0), "Northwest"), "se" | "southeast" => ((1.0, -1.0), "Southeast"), "sw" | "southwest" => ((-1.0, -1.0), "Southwest"), value => { return Err(format!( "Unknown direction: {value}\nValid directions: north, south, east, west, northeast, northwest, southeast, southwest" )); } }; let fly = match args .get(1) .map_or("walk", String::as_str) .to_ascii_lowercase() .as_str() { "walk" => false, "fly" => true, value => return Err(format!("Unknown mode: {value}. Use 'walk' or 'fly'")), }; let vector = Vector3::new_with_single_single_single(direction.0, direction.1, 0.0) .map_err(|_| "Invalid direction")?; let result = backend .world_mutate( Mutation::Movement(Movement::Cross(vector, fly)), cancellation, ) .await?; Ok(format!( "{result} {name} by {}", if fly { "flying" } else { "walking" } )) } async fn follow( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.len() == 1 && args[0].eq_ignore_ascii_case("off") { backend .world_mutate(Mutation::Movement(Movement::Follow(None)), cancellation) .await?; return Ok("Following is off".into()); } if args.len() != 2 { return Err("Usage: follow [FirstName LastName]/off --confirm".into()); } let name = args.join(" "); let id = backend .world_resolve_avatar(&name, cancellation.clone()) .await? .ok_or_else(|| format!("Unable to find {name}"))?; let snapshot = backend.world_snapshot(cancellation.clone()).await?; let position = snapshot .avatars .iter() .find(|avatar| avatar.id == id) .map(|avatar| avatar.position) .ok_or_else(|| format!("Unable to locate {name} in the current simulator"))?; backend .world_mutate( Mutation::Movement(Movement::Follow(Some((id, position)))), cancellation, ) .await?; Ok(format!("Following {name}")) } async fn goto( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; let destination = args.join(" "); let parts: Vec<_> = destination.split('/').collect(); if parts.len() != 4 { return Err("Usage: goto sim/x/y/z --confirm".into()); } let position = parse_vector( &parts[1..] .iter() .map(|part| (*part).to_owned()) .collect::>(), "Usage: goto sim/x/y/z --confirm", )?; backend .world_mutate( Mutation::Movement(Movement::TeleportRegion(parts[0].into(), position)), cancellation, ) .await?; Ok(format!("Teleported to {}", parts[0])) } async fn goto_landmark( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.len() != 1 { return Err("Usage: goto_landmark [UUID] --confirm".into()); } let id = parse_uuid(&args[0])?; backend .world_mutate( Mutation::Movement(Movement::TeleportLandmark(id)), cancellation, ) .await?; Ok("Teleport Successful".into()) } async fn location( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: location".into()); } let snapshot = backend.world_snapshot(cancellation).await?; Ok(format!( "CurrentSim: '{}' Position: {:?}", snapshot.region_name, snapshot.position )) } async fn move_to( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; let local = parse_vector(&args, "Usage: moveto x y z --confirm")?; let snapshot = backend.world_snapshot(cancellation.clone()).await?; let (x, y) = region_origin(snapshot.region_handle)?; let global = [ f64::from(local.x) + f64::from(x), f64::from(local.y) + f64::from(y), f64::from(local.z), ]; backend .world_mutate( Mutation::Movement(Movement::AutoPilot { local, global }), cancellation, ) .await?; Ok(format!( "Attempting to move to <{},{},{}>", global[0], global[1], global[2] )) } async fn sit( backend: &B, args: &[String], explicit: Option<()>, cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; let snapshot = backend.world_snapshot(cancellation.clone()).await?; let prim = if explicit.is_some() { if args.len() != 1 { return Err("Usage: siton UUID --confirm".into()); } let id = parse_uuid(&args[0])?; snapshot .primitives .iter() .find(|prim| prim.id == id) .ok_or_else(|| format!("Couldn't find a prim to sit on with UUID {id}"))? } else { if !args.is_empty() { return Err("Usage: sit --confirm".into()); } snapshot .primitives .iter() .min_by(|left, right| { distance(snapshot.position, left.position) .total_cmp(&distance(snapshot.position, right.position)) }) .ok_or("Couldn't find a nearby prim to sit on")? }; let distance = distance(snapshot.position, prim.position); backend .world_mutate(Mutation::Movement(Movement::Sit(prim.id)), cancellation) .await?; Ok(if explicit.is_some() { format!("Requested to sit on prim {} ({})", prim.id, prim.local_id) } else { format!( "Sat on {} ({}). Distance: {distance}", prim.id, prim.local_id ) }) } fn distance(left: Vector3, right: Vector3) -> f32 { let x = left.x - right.x; let y = left.y - right.y; let z = left.z - right.z; (x * x + y * y + z * z).sqrt() } fn region_origin(handle: u64) -> Result<(u32, u32), String> { let x = u32::try_from(handle >> 32).map_err(|_| "Invalid region handle")?; let y = u32::try_from(handle & u64::from(u32::MAX)).map_err(|_| "Invalid region handle")?; Ok((x, y)) } async fn turn_to( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; let target = parse_vector(&args, "Usage: turnto x y z --confirm")?; backend .world_mutate(Mutation::Movement(Movement::Turn(target)), cancellation) .await?; Ok(format!("Turned to {target:?}")) } async fn wind( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: wind".into()); } let snapshot = backend.world_snapshot(cancellation).await?; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let (x, y) = ( snapshot.position.x.clamp(0.0, 255.0) as usize / 16, snapshot.position.y.clamp(0.0, 255.0) as usize / 16, ); let wind = snapshot .wind .as_ref() .and_then(|values| values.get(y * 16 + x)) .ok_or("Wind data is not available")?; Ok(format!("Local wind speed is {wind:?}")) } async fn agent_locations( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() > 1 { return Err("Usage: agentlocations [regionhandle]".into()); } let initial = backend.world_snapshot(cancellation.clone()).await?; let handle = args .first() .map_or(Ok(initial.region_handle), |arg| arg.parse::()) .map_err(|_| "Usage: agentlocations [regionhandle]")?; let snapshot = backend .world_query(Query::AgentLocations(handle), cancellation) .await?; let locations = snapshot .locations .get(&handle) .filter(|values| !values.is_empty()) .ok_or("Failed to fetch agent locations")?; let mut output = String::from("Agent locations:\n"); for location in locations.iter().take(MAX_QUERY_RESULTS) { output.push_str(&format!( "{} avatar(s) at {},{}\n", location.count, location.x, location.y )); } Ok(output) } async fn find_sim( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.is_empty() { return Err("Usage: findsim [Simulator Name]".into()); } let name = args.join(" ").trim().to_ascii_lowercase(); let snapshot = backend .world_query(Query::FindRegion(name.clone()), cancellation) .await?; snapshot .regions .iter() .find(|region| region.name.eq_ignore_ascii_case(&name)) .map(|region| { format!( "{}: handle={} ({},{})", region.name, region.region_handle, region.x, region.y ) }) .ok_or_else(|| format!("Lookup of {name} failed")) } async fn grid_layer( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: gridlayer".into()); } let snapshot = backend.world_query(Query::GridLayer, cancellation).await?; let mut output = String::new(); for layer in snapshot.layers.iter().take(MAX_QUERY_RESULTS) { output.push_str(&format!( "Layer({}) Bottom: {} Left: {} Top: {} Right: {}\n", layer.image_id, layer.bottom, layer.left, layer.top, layer.right )); } output.push_str(&format!("Received {} layer chunks", snapshot.layers.len())); Ok(output) } async fn grid_map( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: gridmap".into()); } let snapshot = backend.world_query(Query::GridMap, cancellation).await?; let mut regions = snapshot.regions; regions.sort_by(|a, b| a.name.cmp(&b.name)); let mut output = String::new(); for region in regions.iter().take(MAX_QUERY_RESULTS) { output.push_str(&format!( "{}: handle={} ({},{}) agents={}\n", region.name, region.region_handle, region.x, region.y, region.agents )); } output.push_str(&format!("Received {} grid regions", regions.len())); Ok(output) } async fn parcel_info( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: parcelinfo".into()); } let snapshot = backend.world_query(Query::Parcels, cancellation).await?; let mut output = format!( "Downloaded {} Parcels in {}\n", snapshot.parcels.len(), snapshot.region_name ); for view in snapshot.parcels.iter().take(MAX_QUERY_RESULTS) { let p = &view.parcel; output.push_str(&format!("Parcel[{}]: Name: \"{}\", Description: \"{}\" ACLBlacklist Count: {}, ACLWhiteList Count: {} Traffic: {}\n", p.local_id, p.name, p.desc, p.access_black_list.len(), p.access_white_list.len(), p.dwell)); } Ok(output) } async fn parcel_details( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 1 { return Err("Usage: parceldetails parcelID (use parcelinfo to get ID)".into()); } let id = args[0] .parse::() .map_err(|_| "Usage: parceldetails parcelID (use parcelinfo to get ID)")?; let snapshot = backend.world_query(Query::Parcels, cancellation).await?; let p=&snapshot.parcels.iter().find(|view| view.parcel.local_id==id).ok_or_else(||format!("Unable to find Parcel {} in Parcels Dictionary, Did you run parcelinfo to populate the dictionary first?",args[0]))?.parcel; Ok(format!( "LocalID = {}\nName = {}\nDescription = {}\nOwnerID = {}\nGroupID = {}\nArea = {}\nDwell = {}\nTotalPrims = {}\nMaxPrims = {}\nSalePrice = {}\nLanding = {:?}\nFlags = {:?}", p.local_id, p.name, p.desc, p.owner_id, p.group_id, p.area, p.dwell, p.total_prims, p.max_prims, p.sale_price, p.landing, p.flags )) } async fn prim_owners( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 1 { return Err("Usage: primowners parcelID (use parcelinfo to get ID)".into()); } let id = args[0] .parse::() .map_err(|_| "Usage: primowners parcelID (use parcelinfo to get ID)")?; let snapshot = backend .world_query(Query::ParcelOwners(id), cancellation) .await?; let view=snapshot.parcels.iter().find(|view|view.parcel.local_id==id).ok_or_else(||format!("Unable to find Parcel {} in Parcels Dictionary, Did you run parcelinfo to populate the dictionary first?",args[0]))?; if view.owners.is_empty() { return Ok("No primitive owners returned".into()); } let mut output = String::new(); for owner in view.owners.iter().take(MAX_QUERY_RESULTS) { output.push_str(&format!("Owner: {} Count: {}\n", owner.owner, owner.count)); } Ok(output) } async fn select_objects( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 2 { return Err("Usage: selectobjects parcelID OwnerUUID (use parcelinfo to get ID, use primowners to get ownerUUID)".into()); } let id=args[0].parse::().map_err(|_|"Usage: selectobjects parcelID OwnerUUID (use parcelinfo to get ID, use primowners to get ownerUUID)")?; let owner = parse_uuid(&args[1])?; let snapshot = backend .world_query(Query::ParcelObjects(id, owner), cancellation) .await?; let ids = snapshot .parcels .iter() .find(|view| view.parcel.local_id == id) .and_then(|view| view.selected.get(&owner)) .cloned() .unwrap_or_default(); let mut output = ids .iter() .take(MAX_QUERY_RESULTS) .map(u32::to_string) .collect::>() .join(" "); if !output.is_empty() { output.push(' '); } output.push_str(&format!("Found a total of {} Objects", ids.len())); Ok(output) } async fn syntax_id( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: syntaxid".into()); } let mut syntax = backend.world_snapshot(cancellation).await?.syntax; syntax.sort(); syntax.dedup(); Ok(format!("LSL Tokens:\n{}", syntax.join("\n"))) } async fn estate_covenant( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() > 1 { return Err("Usage: getestatecovenant [timeout]".into()); } let seconds = args .first() .map_or(Ok(20), |v| v.parse::()) .map_err(|_| "Usage: getestatecovenant [timeout]")?; if !(1..=120).contains(&seconds) { return Err("Covenant timeout must be between 1 and 120 seconds".into()); } let snapshot = backend .world_query( Query::EstateCovenant(Duration::from_secs(seconds)), cancellation, ) .await?; let estate = snapshot .estate .ok_or("Timeout waiting for covenant info.")?; let mut output = format!( "Estate name: {}\nEstate owner: {}\n", estate.name, estate.owner ); if estate.covenant != UUID::zero() { output.push_str(&format!( "Estate Covenant ID: {}\nEstate Covenant Update Time: {}\nCovenant:\n{}", estate.covenant, estate.timestamp, estate.body )); } Ok(output) } async fn download_terrain( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, true)?; if args.len() > 2 { return Err("Usage: downloadterrain [timeout-ms] [output] --confirm".into()); } let timeout = args .first() .map_or(Ok(120_000), |v| v.parse::()) .map_err(|_| "Usage: downloadterrain [timeout-ms] [output] --confirm")?; if !(1..=300_000).contains(&timeout) { return Err("Terrain timeout must be between 1 and 300000 ms".into()); } let snapshot = backend .world_query( Query::DownloadTerrain(Duration::from_millis(timeout)), cancellation, ) .await?; let estate = snapshot .estate .ok_or("Timeout while waiting for terrain data")?; let path = args.get(1).map_or_else( || PathBuf::from(format!("{}.raw", snapshot.region_name)), PathBuf::from, ); let path = safe_path(path.to_string_lossy().as_ref())?; write_bounded(&path, &estate.terrain)?; Ok(format!( "Terrain file {} ({} bytes) downloaded successfully, written to {}", snapshot.region_name, estate.terrain.len(), path.display() )) } async fn upload_terrain( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, true, true)?; if args.len() != 1 { return Err("Usage: uploadterrain filename --confirm".into()); } let path = safe_path(&args[0])?; let data = read_bounded(&path)?; backend .world_mutate( Mutation::UploadTerrain { name: path .file_name() .and_then(|v| v.to_str()) .unwrap_or("terrain.raw") .into(), data, }, cancellation, ) .await?; Ok("Terrain raw file uploaded and applied".into()) } async fn change_permissions( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.is_empty() || args.len() > 4 { return Err("Usage: changeperms prim-uuid [copy] [mod] [xfer] --confirm".into()); } let root = parse_uuid(&args[0])?; let mut permissions = PermissionMask::NONE; for value in &args[1..] { match value.to_ascii_lowercase().as_str() { "copy" => permissions.0 |= PermissionMask::COPY.0, "mod" => permissions.0 |= PermissionMask::MODIFY.0, "xfer" => permissions.0 |= PermissionMask::TRANSFER.0, _ => return Err("Usage: changeperms prim-uuid [copy] [mod] [xfer] --confirm".into()), } } backend .world_mutate( Mutation::ChangePermissions { root, permissions }, cancellation, ) .await } async fn derez( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.len() != 1 { return Err("Usage: derez [prim-uuid] --confirm".into()); } let id = parse_uuid(&args[0])?; let snapshot = backend.world_snapshot(cancellation.clone()).await?; let prim = snapshot .primitives .iter() .find(|prim| prim.id == id) .ok_or_else(|| format!("Could not find object {id}"))?; let name = prim .properties .as_ref() .map_or("Object", |properties| properties.name.as_str()); backend .world_mutate(Mutation::DeRez(id), cancellation) .await?; Ok(format!("Removing {name} ({id})")) } async fn download_texture( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.is_empty() || args.len() > 3 { return Err("Usage: downloadtexture [texture-uuid] [discardlevel] [output]".into()); } let id = parse_uuid(&args[0])?; let discard = args .get(1) .map_or(Ok(0), |value| value.parse::()) .map_err(|_| "Usage: downloadtexture [texture-uuid] [discardlevel] [output]")?; if !(0..=5).contains(&discard) { return Err("Discard level must be between 0 and 5".into()); } let data = backend.world_texture(id, discard, cancellation).await?; let path = safe_path( args.get(2) .map_or_else(|| format!("{id}.jp2"), Clone::clone) .as_str(), )?; write_bounded(&path, &data)?; let dimensions = libremetaverse_imaging::RustJ2kCodec::decode_bytes( &data, libremetaverse_imaging::J2kDecodeOptions::default(), ) .map_or_else( |_| "undecoded".into(), |image| format!("{}x{}", image.width, image.height), ); Ok(format!("Saved {} ({dimensions})", path.display())) } fn primitive_name(prim: &Primitive) -> &str { prim.properties .as_ref() .map_or("(unknown)", |properties| properties.name.as_str()) } fn primitive_description(prim: &Primitive) -> &str { prim.properties .as_ref() .map_or("(unknown)", |properties| properties.description.as_str()) } fn texture_ids(prim: &Primitive) -> BTreeSet { let mut ids = BTreeSet::new(); if let Some(textures) = &prim.textures { if let Some(face) = &textures.default_texture { ids.insert(face.texture_id()); } for face in textures.face_textures.iter().flatten() { ids.insert(face.texture_id()); } } if let Some(sculpt) = &prim.sculpt && sculpt.sculpt_texture != UUID::zero() { ids.insert(sculpt.sculpt_texture); } ids.remove(&PrimitiveTextureEntry::white_texture()); ids } fn linkset(primitives: &[Primitive], id: UUID) -> Result, String> { let selected = primitives .iter() .find(|prim| prim.id == id) .ok_or_else(|| { format!( "Couldn't find UUID {id} in the objects currently indexed in the current simulator" ) })?; let root = if selected.parent_id == 0 { selected.local_id } else { selected.parent_id }; let mut values: Vec<_> = primitives .iter() .filter(|prim| prim.local_id == root || prim.parent_id == root) .take(MAX_EXPORT_PRIMS + 1) .collect(); if values.len() > MAX_EXPORT_PRIMS { return Err("Linkset exceeds the 10,000 primitive export limit".into()); } values.sort_by_key(|prim| prim.local_id); Ok(values) } async fn export( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 2 { return Err("Usage: export uuid outputfile.xml".into()); } let id = parse_uuid(&args[0])?; let output_path = safe_path(&args[1])?; let snapshot = backend.world_snapshot(cancellation.clone()).await?; let prims = linkset(&snapshot.primitives, id)?; let owner = prims .first() .and_then(|prim| prim.properties.as_ref()) .map_or_else(|| prims[0].owner_id, |properties| properties.owner_id); if owner != backend.world_agent_id() { return Err(format!( "That object is owned by {owner}, we don't have permission to export it" )); } let values = prims .iter() .map(|prim| prim.get_osd()) .collect::, _>>() .map_err(|_| "Could not serialize primitive linkset")?; let xml = OSDParser::serialize_llsd_xml_string(OSD::Array(values)) .map_err(|_| "Could not serialize primitive linkset")?; write_bounded(&output_path, xml.as_bytes())?; let texture_directory = output_path.parent().unwrap_or_else(|| Path::new(".")); let mut textures = BTreeSet::new(); for prim in &prims { textures.extend(texture_ids(prim)); } let mut downloaded = 0usize; for texture in textures { let data = backend .world_texture(texture, 0, cancellation.clone()) .await?; let path = texture_directory.join(format!("{texture}.jp2")); write_bounded(&path, &data)?; let image = libremetaverse_imaging::RustJ2kCodec::decode_bytes( &data, libremetaverse_imaging::J2kDecodeOptions::default(), ) .map_err(|_| format!("Failed to decode exported texture {texture}"))?; let tga_path = texture_directory.join(format!("{texture}.tga")); write_bounded(&tga_path, &super::inventory::encode_tga(&image)?)?; downloaded += 1; } Ok(format!( "Exported {} prims to {}; downloaded {downloaded} textures", prims.len(), output_path.display() )) } fn particle_lsl(prim: &Primitive) -> Result { let particle = &prim.particle_sys; if particle.crc == 0 { return Err(format!( "Prim {} does not have a particle system", prim.local_id )); } let pattern = match particle.pattern { PrimitiveParticleSystemSourcePattern::DROP => "PSYS_SRC_PATTERN_DROP", PrimitiveParticleSystemSourcePattern::EXPLODE => "PSYS_SRC_PATTERN_EXPLODE", PrimitiveParticleSystemSourcePattern::ANGLE => "PSYS_SRC_PATTERN_ANGLE", PrimitiveParticleSystemSourcePattern::ANGLE_CONE => "PSYS_SRC_PATTERN_ANGLE_CONE", PrimitiveParticleSystemSourcePattern::ANGLE_CONE_EMPTY => { "PSYS_SRC_PATTERN_ANGLE_CONE_EMPTY" } _ => "0", }; let acceleration = lsl_vector(particle.part_acceleration); let omega = lsl_vector(particle.angular_velocity); Ok(format!( "default\n{{\n state_entry()\n {{\n llParticleSystem([\n PSYS_PART_FLAGS, {},\n PSYS_SRC_PATTERN, {pattern},\n PSYS_PART_START_ALPHA, {:.5},\n PSYS_PART_END_ALPHA, {:.5},\n PSYS_PART_START_SCALE, <{:.5}, {:.5}, 0>,\n PSYS_PART_END_SCALE, <{:.5}, {:.5}, 0>,\n PSYS_PART_MAX_AGE, {:.5},\n PSYS_SRC_MAX_AGE, {:.5},\n PSYS_SRC_ACCEL, {},\n PSYS_SRC_BURST_PART_COUNT, {},\n PSYS_SRC_BURST_RADIUS, {:.5},\n PSYS_SRC_BURST_RATE, {:.5},\n PSYS_SRC_BURST_SPEED_MIN, {:.5},\n PSYS_SRC_BURST_SPEED_MAX, {:.5},\n PSYS_SRC_INNERANGLE, {:.5},\n PSYS_SRC_OUTERANGLE, {:.5},\n PSYS_SRC_OMEGA, {},\n PSYS_SRC_TEXTURE, (key)\"{}\",\n PSYS_SRC_TARGET_KEY, (key)\"{}\"\n ]);\n }}\n}}\n", particle.part_data_flags.0, particle.part_start_color.a, particle.part_end_color.a, particle.part_start_scale_x, particle.part_start_scale_y, particle.part_end_scale_x, particle.part_end_scale_y, particle.part_max_age, particle.max_age, acceleration, particle.burst_part_count, particle.burst_radius, particle.burst_rate, particle.burst_speed_min, particle.burst_speed_max, particle.inner_angle, particle.outer_angle, omega, particle.texture, particle.target )) } fn lsl_vector(value: Vector3) -> String { format!("<{:.5}, {:.5}, {:.5}>", value.x, value.y, value.z) } async fn export_particles( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 1 { return Err("Usage: exportparticles [prim-uuid]".into()); } let id = parse_uuid(&args[0])?; let snapshot = backend.world_snapshot(cancellation).await?; let prim = snapshot .primitives .iter() .find(|prim| prim.id == id) .ok_or_else(|| format!("Could not find {id} object"))?; particle_lsl(prim) } async fn find_objects( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.is_empty() || args.len() > 2 { return Err("Usage: findobjects [radius] ".into()); } let radius = args[0] .parse::() .map_err(|_| "Usage: findobjects [radius] ")?; if !radius.is_finite() || !(0.0..=4096.0).contains(&radius) { return Err("Radius must be between 0 and 4096 metres".into()); } let search = args.get(1).map_or("", String::as_str); let snapshot = backend.world_snapshot(cancellation).await?; let mut output = String::new(); let mut count = 0; for prim in snapshot .primitives .iter() .filter(|prim| distance(snapshot.position, prim.position) < radius) .take(MAX_QUERY_RESULTS) { let name = primitive_name(prim); if search.is_empty() || name.contains(search) { output.push_str(&format!("Object '{name}': {}\n", prim.id)); count += 1; } } output.push_str(&format!("Done searching; found {count} objects")); Ok(output) } async fn find_texture( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 2 { return Err("Usage: findtexture [face-index] [texture-uuid]".into()); } let face = args[0] .parse::() .map_err(|_| "Usage: findtexture [face-index] [texture-uuid]")?; if face >= PrimitiveTextureEntry::MAX_FACES as usize { return Err("Face index is outside the supported range".into()); } let id = parse_uuid(&args[1])?; let snapshot = backend.world_snapshot(cancellation).await?; let mut output = String::new(); let mut count = 0; for prim in &snapshot.primitives { if let Some(texture) = prim .textures .as_ref() .and_then(|textures| textures.face_textures.get(face)) .and_then(Option::as_ref) && texture.texture_id() == id { output.push_str(&format!( "Primitive {} ({}) has face index {face} set to {id}\n", prim.id, prim.local_id )); count += 1; } } output.push_str(&format!("Done searching; found {count} faces")); Ok(output) } async fn import( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, true, false)?; if args.is_empty() || args.len() > 2 { return Err("Usage: import inputfile.xml [usegroup] --confirm".into()); } if args.len() == 2 && !args[1].eq_ignore_ascii_case("usegroup") { return Err("Usage: import inputfile.xml [usegroup] --confirm".into()); } let path = safe_path(&args[0])?; let data = read_bounded(&path)?; let osd = OSDParser::deserialize_llsd_xml_with_bytes(data) .map_err(|_| format!("Failed to deserialize {}", path.display()))?; let OSD::Array(values) = osd else { return Err("Import document must contain an LLSD array".into()); }; if values.is_empty() || values.len() > MAX_EXPORT_PRIMS { return Err("Import must contain between 1 and 10,000 primitives".into()); } let primitives = values .into_iter() .map(Primitive::from_osd) .collect::, _>>() .map_err(|_| format!("Failed to deserialize {}", path.display()))?; backend .world_mutate( Mutation::Import { primitives, use_group: args.len() == 2, }, cancellation, ) .await?; Ok("Import complete.".into()) } async fn prim_count( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if !args.is_empty() { return Err("Usage: primcount".into()); } let snapshot = backend.world_snapshot(cancellation).await?; Ok(format!( "{} (Avatars: {} Primitives: {})\nTracking a total of {} objects", snapshot.region_name, snapshot.avatars.len(), snapshot.primitives.len(), snapshot.avatars.len() + snapshot.primitives.len() )) } async fn prim_info( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 1 { return Err("Usage: priminfo [prim-uuid]".into()); } let id = parse_uuid(&args[0])?; let snapshot = backend.world_snapshot(cancellation).await?; let prim = snapshot .primitives .iter() .find(|prim| prim.id == id) .ok_or_else(|| format!("Could not find object {id}"))?; let mut output = format!( "ID: {}\nLocalID: {}\nParentID: {}\nName: {}\nDescription: {}\nPosition: {:?}\nScale: {:?}\nRotation: {:?}\nFlags: {:?}\nText: {}\nParticleCRC: {}\n", prim.id, prim.local_id, prim.parent_id, primitive_name(prim), primitive_description(prim), prim.position, prim.scale, prim.rotation, prim.flags, prim.text, prim.particle_sys.crc ); for texture in texture_ids(prim) { output.push_str(&format!("Texture: {texture}\n")); } if let Some(properties) = &prim.properties { output.push_str(&format!("OwnerID: {}\nCreatorID: {}\nCategory: {:?}\nFolderID: {}\nFromTaskID: {}\nInventorySerial: {}\nItemID: {}\n",properties.owner_id,properties.creator_id,properties.category,properties.folder_id,properties.from_task_id,properties.inventory_serial,properties.item_id)); } output.push_str("Done."); Ok(output) } async fn prim_regex( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.is_empty() { return Err("Usage: primregex [text predicate]".into()); } let predicate = args.join(" "); if predicate.len() > 1024 { return Err("Predicate exceeds 1,024 bytes".into()); } let regex = regex::RegexBuilder::new(&predicate) .case_insensitive(true) .size_limit(1024 * 1024) .build() .map_err(|error| format!("Error searching: {error}"))?; let snapshot = backend.world_snapshot(cancellation).await?; let mut output = format!( "Searching prim for [{predicate}] ({} prims loaded in simulator)\n", snapshot.primitives.len() ); let mut count = 0; for prim in &snapshot.primitives { if regex.is_match(&prim.text) || regex.is_match(primitive_name(prim)) || regex.is_match(primitive_description(prim)) { output.push_str(&format!( "NAME={}\nID = {}\nFLAGS = {:?}\nTEXT = '{}'\nDESC='{}'\n", primitive_name(prim), prim.id, prim.flags, prim.text, primitive_description(prim) )); count += 1; } } output.push_str(&format!("Done searching; found {count} objects")); Ok(output) } async fn textures( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { if args.len() != 1 { return Err("Usage: textures [on/off]".into()); } let enabled = match args[0].to_ascii_lowercase().as_str() { "on" => true, "off" => false, _ => return Err("Usage: textures [on/off]".into()), }; backend .world_mutate(Mutation::SetTextures(enabled), cancellation) .await?; Ok(format!( "Texture downloading is {}", if enabled { "on" } else { "off" } )) } fn parse_tree(value: &str) -> Option { match value.to_ascii_lowercase().as_str() { "beachgrass1" => Some(Tree::BeachGrass1), "cypress1" => Some(Tree::Cypress1), "cypress2" => Some(Tree::Cypress2), "dogwood" => Some(Tree::Dogwood), "eelgrass" => Some(Tree::Eelgrass), "eucalyptus" => Some(Tree::Eucalyptus), "fern" => Some(Tree::Fern), "kelp1" => Some(Tree::Kelp1), "kelp2" => Some(Tree::Kelp2), "oak" => Some(Tree::Oak), "palm1" => Some(Tree::Palm1), "palm2" => Some(Tree::Palm2), "pine1" => Some(Tree::Pine1), "pine2" => Some(Tree::Pine2), "plumeria" => Some(Tree::Plumeria), "seasword" => Some(Tree::SeaSword), "tropicalbush1" => Some(Tree::TropicalBush1), "tropicalbush2" => Some(Tree::TropicalBush2), "winteraspen" => Some(Tree::WinterAspen), "winterpine1" => Some(Tree::WinterPine1), "winterpine2" => Some(Tree::WinterPine2), _ => None, } } async fn tree( backend: &B, args: &[String], cancellation: CancellationToken, ) -> Result { let args = authorize(backend, args, false, false)?; if args.len() != 1 { return Err("Usage: tree [BeachGrass1,Cypress1,Cypress2,Dogwood,Eelgrass,Eucalyptus,Fern,Kelp1,Kelp2,Oak,Palm1,Palm2,Pine1,Pine2,Plumeria,SeaSword,TropicalBush1,TropicalBush2,WinterAspen,WinterPine1,WinterPine2] --confirm".into()); } let species = parse_tree(&args[0]).ok_or("Type !tree for usage")?; backend .world_mutate(Mutation::Tree(species), cancellation) .await?; Ok(format!("Attempted to rez a {} tree", args[0])) } impl Backend for FakeBackend { fn world_state(&self) -> &Mutex { &self.world_state } fn world_agent_id(&self) -> UUID { self.id } fn world_snapshot<'a>( &'a self, cancellation: CancellationToken, ) -> BackendFuture<'a, Result> { Box::pin(async move { if cancellation.is_cancellation_requested() { Err("World query cancelled".into()) } else { Ok(lock(&self.world_state).fake.clone()) } }) } fn world_query<'a>( &'a self, query: Query, cancellation: CancellationToken, ) -> BackendFuture<'a, Result> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("World query cancelled".into()); } self.record(format!("CALL world-query {}", query_name(&query))); let snapshot = lock(&self.world_state).fake.clone(); match query { Query::FindRegion(name) if !snapshot .regions .iter() .any(|region| region.name.eq_ignore_ascii_case(&name)) => { Err(format!("Lookup of {name} failed")) } Query::EstateCovenant(_) | Query::DownloadTerrain(_) if snapshot.estate.is_none() => { Err("Estate fixture is missing".into()) } _ => Ok(snapshot), } }) } fn world_mutate<'a>( &'a self, mutation: Mutation, cancellation: CancellationToken, ) -> BackendFuture<'a, Result> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("World mutation cancelled".into()); } let mut state = lock(&self.world_state); let result = match mutation { Mutation::Movement(action) => apply_fake_movement(self, &mut state, action)?, Mutation::ChangePermissions { root, permissions } => { let root_local = state .fake .primitives .iter() .find(|prim| prim.id == root) .map(|prim| { if prim.parent_id == 0 { prim.local_id } else { prim.parent_id } }) .ok_or_else(|| format!("Cannot find requested object {root}"))?; let mut count = 0; for prim in &mut state.fake.primitives { if prim.local_id == root_local || prim.parent_id == root_local { if let Some(properties) = &mut prim.properties { properties.permissions.next_owner_mask = permissions; } count += 1; } } self.record(format!( "CALL object-permissions {root} {:08x}", permissions.0 )); format!( "Set permissions to {permissions:?} on {count} objects and 0 inventory items" ) } Mutation::DeRez(id) => { let before = state.fake.primitives.len(); state.fake.primitives.retain(|prim| prim.id != id); if before == state.fake.primitives.len() { return Err(format!("Could not find object {id}")); } self.record(format!("CALL object-derez {id}")); "Object removed".into() } Mutation::Import { mut primitives, use_group, } => { let base = state .fake .primitives .iter() .map(|prim| prim.local_id) .max() .unwrap_or(100); let local_map = primitives .iter() .enumerate() .map(|(index, prim)| { let local = base .saturating_add(u32::try_from(index).unwrap_or(u32::MAX)) .saturating_add(1); (prim.local_id, local) }) .collect::>(); for prim in &mut primitives { let old_parent = prim.parent_id; prim.local_id = *local_map .get(&prim.local_id) .ok_or("Imported primitive ID mapping is incomplete")?; prim.parent_id = if old_parent == 0 { 0 } else { *local_map .get(&old_parent) .ok_or("Imported primitive parent is outside the linkset")? }; prim.id = UUID::new_with_u_int64( 0x9300_0000_0000_0000 | u64::from(prim.local_id), ) .map_err(|_| "Creating imported primitive UUID")?; prim.position.x += state.fake.position.x; prim.position.y += state.fake.position.y; prim.position.z += state.fake.position.z + 3.0; } let count = primitives.len(); state.fake.primitives.extend(primitives); self.record(format!( "CALL object-import count={count} use-group={use_group}" )); format!("Imported {count} primitives") } Mutation::SetTextures(enabled) => { state.textures_enabled = enabled; self.record(format!("CALL texture-stream enabled={enabled}")); "Texture setting updated".into() } Mutation::Tree(species) => { self.record(format!("CALL object-tree {species:?}")); "Tree rez requested".into() } Mutation::UploadTerrain { name, data } => { let estate = state .fake .estate .as_mut() .ok_or("Estate fixture is missing")?; estate.terrain = data; self.record(format!("CALL estate-upload-terrain {name}")); "Terrain upload completed".into() } }; Ok(result) }) } fn world_texture<'a>( &'a self, id: UUID, _discard: i32, cancellation: CancellationToken, ) -> BackendFuture<'a, Result, String>> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("Texture download cancelled".into()); } self.record(format!("CALL texture-download {id}")); lock(&self.world_state) .fake .assets .get(&id) .cloned() .ok_or_else(|| format!("Download failed or texture not found: {id}")) }) } fn world_resolve_avatar<'a>( &'a self, name: &'a str, cancellation: CancellationToken, ) -> BackendFuture<'a, Result, String>> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("Avatar lookup cancelled".into()); } Ok(lock(&self.world_state) .fake .avatars .iter() .find(|avatar| avatar.name.eq_ignore_ascii_case(name)) .map(|avatar| avatar.id)) }) } fn apply_world_fixture(&self, fields: &[&str]) -> Result<(), &'static str> { apply_fixture(self, fields) } } fn query_name(query: &Query) -> &'static str { match query { Query::AgentLocations(_) => "agent-locations", Query::FindRegion(_) => "find-region", Query::GridLayer => "grid-layer", Query::GridMap => "grid-map", Query::Parcels => "parcels", Query::ParcelOwners(_) => "parcel-owners", Query::ParcelObjects(_, _) => "parcel-objects", Query::EstateCovenant(_) => "estate-covenant", Query::DownloadTerrain(_) => "download-terrain", } } fn apply_fake_movement( backend: &FakeBackend, state: &mut State, action: Movement, ) -> Result { let value = match action { Movement::Pulse(direction, duration) => { backend.record(format!( "CALL movement-{} duration-ms={}", direction_name(direction), duration.as_millis() )); format!("movement {} complete", direction_name(direction)) } Movement::Crouch(enabled) => { backend.record(format!("CALL movement-crouch {enabled}")); "crouch updated".into() } Movement::Fly(enabled) => { backend.record(format!("CALL movement-fly {enabled}")); "flight updated".into() } Movement::FlyTo(position, duration) => { state.fake.position = position; backend.record(format!( "CALL movement-flyto {position:?} duration-ms={}", duration.as_millis() )); "FlyTo complete".into() } Movement::Follow(target) => { state.follow_target = target.as_ref().map(|value| value.0); backend.record(format!( "CALL movement-follow {}", target .as_ref() .map_or_else(|| "off".into(), |(id, _)| id.to_string()) )); "Follow updated".into() } Movement::GoHome => { state.fake.position = Vector3::new_with_single_single_single(128.0, 128.0, 25.0) .map_err(|_| "Invalid home position")?; backend.record("CALL teleport-home".into()); "Teleport complete".into() } Movement::TeleportRegion(name, position) => { state.fake.region_name.clone_from(&name); state.fake.position = position; backend.record(format!("CALL teleport-region {name} {position:?}")); "Teleport complete".into() } Movement::TeleportLandmark(id) => { backend.record(format!("CALL teleport-landmark {id}")); "Teleport complete".into() } Movement::Jump => { backend.record("CALL movement-jump".into()); "Jump complete".into() } Movement::AutoPilot { local, global } => { state.fake.position = local; backend.record(format!("CALL movement-autopilot {global:?}")); "Autopilot started".into() } Movement::SetHome => { backend.record("CALL movement-set-home".into()); "Home set".into() } Movement::Sit(id) => { backend.record(format!("CALL movement-sit {id}")); "Sit requested".into() } Movement::Stand => { backend.record("CALL movement-stand".into()); "Stand requested".into() } Movement::Turn(position) => { backend.record(format!("CALL movement-turn {position:?}")); "Turn complete".into() } Movement::Cross(direction, fly) => { state.fake.position.x = if direction.x < 0.0 { 246.0 } else { 10.0 }; state.fake.position.y = if direction.y < 0.0 { 246.0 } else { 10.0 }; backend.record(format!("CALL movement-cross {direction:?} fly={fly}")); "Successfully crossed region border".into() } }; Ok(value) } pub(super) fn apply_fake_directive( manager: &ClientManager, fields: &[&str], ) -> Option> { if !fields .first() .is_some_and(|name| name.starts_with("world-")) { return None; } let client = fields .get(1) .and_then(|value| UUID::new_with_string((*value).into()).ok()); let Some(client) = client else { return Some(Err("world fixture client UUID is invalid")); }; let Some(client) = manager.clients.get(&client) else { return Some(Err("world fixture client is not registered")); }; Some(client.backend.apply_world_fixture(fields)) } fn fixture_uuid(value: &str) -> Result { UUID::new_with_string(value.into()).map_err(|_| "world fixture UUID is invalid") } fn fixture_number(value: &str) -> Result { value.parse().map_err(|_| "world fixture number is invalid") } fn fixture_vector(x: &str, y: &str, z: &str) -> Result { Vector3::new_with_single_single_single( fixture_number(x)?, fixture_number(y)?, fixture_number(z)?, ) .map_err(|_| "world fixture vector is invalid") } fn fixture_hex(value: &str) -> Result, &'static str> { if !value.len().is_multiple_of(2) || u64::try_from(value.len() / 2).unwrap_or(u64::MAX) > MAX_FILE_BYTES { return Err("world asset fixture is invalid or too large"); } value .as_bytes() .chunks_exact(2) .map(|pair| { let high = hex_nibble(pair[0]).ok_or("world asset fixture is invalid")?; let low = hex_nibble(pair[1]).ok_or("world asset fixture is invalid")?; Ok(high << 4 | low) }) .collect() } const fn hex_nibble(value: u8) -> Option { match value { b'0'..=b'9' => Some(value - b'0'), b'a'..=b'f' => Some(value - b'a' + 10), b'A'..=b'F' => Some(value - b'A' + 10), _ => None, } } fn apply_fixture(backend: &FakeBackend, fields: &[&str]) -> Result<(), &'static str> { let mut state = lock(&backend.world_state); let world = &mut state.fake; match fields { ["world-region", _, handle, name, x, y, z, wind_x, wind_y] => { world.region_handle = fixture_number(handle)?; world.region_name = (*name).into(); world.position = fixture_vector(x, y, z)?; let wind = Vector2::new_with_single_single(fixture_number(wind_x)?, fixture_number(wind_y)?) .map_err(|_| "world fixture wind is invalid")?; world.wind = Some(vec![wind; 256]); } ["world-avatar", _, id, name, x, y, z] => world.avatars.push(AvatarView { id: fixture_uuid(id)?, name: (*name).into(), position: fixture_vector(x, y, z)?, }), [ "world-prim", _, id, local, parent, owner, name, description, x, y, z, texture, particle, ] => { let mut prim = Primitive::new_with_constructor() .map_err(|_| "world primitive fixture could not be created")?; prim.id = fixture_uuid(id)?; prim.local_id = fixture_number(local)?; prim.parent_id = fixture_number(parent)?; prim.owner_id = fixture_uuid(owner)?; prim.position = fixture_vector(x, y, z)?; prim.scale = Vector3::new_with_single_single_single(1.0, 1.0, 1.0) .map_err(|_| "world primitive fixture scale is invalid")?; let mut properties = PrimitiveObjectProperties::new() .map_err(|_| "world primitive properties fixture could not be created")?; properties.object_id = prim.id; properties.owner_id = prim.owner_id; properties.creator_id = prim.owner_id; properties.name = (*name).into(); properties.description = (*description).into(); properties.permissions = libremetaverse::Permissions::full_permissions(); prim.properties = Some(properties); let texture = fixture_uuid(texture)?; let mut textures = PrimitiveTextureEntry::new_with_uuid(texture) .map_err(|_| "world texture fixture could not be created")?; textures .create_face(0) .map_err(|_| "world texture face fixture could not be created")? .set_texture_id(texture); prim.textures = Some(textures); if *particle == "true" { prim.particle_sys.crc = 1; prim.particle_sys.pattern = PrimitiveParticleSystemSourcePattern::DROP; prim.particle_sys.texture = texture; } else if *particle != "false" { return Err("world particle fixture must be true or false"); } world.primitives.push(prim); } [ "world-parcel", _, local, owner, name, description, dwell, area, total, max, ] => { let mut parcel = Parcel::new(fixture_number(local)?) .map_err(|_| "world parcel fixture could not be created")?; parcel.owner_id = fixture_uuid(owner)?; parcel.name = (*name).into(); parcel.desc = (*description).into(); parcel.dwell = fixture_number(dwell)?; parcel.area = fixture_number(area)?; parcel.total_prims = fixture_number(total)?; parcel.max_prims = fixture_number(max)?; world.parcels.push(ParcelView { parcel, owners: Vec::new(), selected: HashMap::new(), }); } ["world-parcel-owner", _, local, owner, count] => { let local: i32 = fixture_number(local)?; world .parcels .iter_mut() .find(|view| view.parcel.local_id == local) .ok_or("world parcel fixture must precede owner")? .owners .push(OwnerView { owner: fixture_uuid(owner)?, count: fixture_number(count)?, }); } ["world-parcel-object", _, local, owner, ids] => { let local: i32 = fixture_number(local)?; let view = world .parcels .iter_mut() .find(|view| view.parcel.local_id == local) .ok_or("world parcel fixture must precede object")?; let values = if *ids == "-" { Vec::new() } else { ids.split(',') .map(fixture_number) .collect::>()? }; view.selected.insert(fixture_uuid(owner)?, values); } ["world-grid-region", _, name, handle, x, y, agents] => world.regions.push(GridRegion { access: libremetaverse::SimAccess::UNKNOWN, agents: fixture_number(agents)?, map_image_id: UUID::zero(), name: (*name).into(), region_flags: libremetaverse::RegionFlags(0), region_handle: fixture_number(handle)?, water_height: 20, x: fixture_number(x)?, y: fixture_number(y)?, }), ["world-layer", _, image, bottom, left, top, right] => world.layers.push(GridLayer { bottom: fixture_number(bottom)?, image_id: fixture_uuid(image)?, left: fixture_number(left)?, right: fixture_number(right)?, top: fixture_number(top)?, }), ["world-agent-location", _, handle, count, x, y] => world .locations .entry(fixture_number(handle)?) .or_default() .push(AgentLocation { count: fixture_number(count)?, x: fixture_number(x)?, y: fixture_number(y)?, }), [ "world-estate", _, name, owner, covenant, timestamp, body, terrain, ] => { world.estate = Some(EstateView { name: (*name).into(), owner: fixture_uuid(owner)?, covenant: fixture_uuid(covenant)?, timestamp: fixture_number(timestamp)?, body: (*body).into(), terrain: fixture_hex(terrain)?, }); } ["world-asset", _, id, data] => { world.assets.insert(fixture_uuid(id)?, fixture_hex(data)?); } ["world-syntax", _, tokens] => world.syntax.extend(tokens.split(',').map(str::to_owned)), _ => return Err("invalid world fake directive"), } Ok(()) } pub(super) fn install_live(backend: &Arc, subscriptions: &mut Vec) { let weak = Arc::downgrade(backend); let grid = lock(&backend.client).grid(); subscriptions.push( grid.subscribe_coarse_location_update(Arc::new(move |event| { let Some(backend) = weak.upgrade() else { return; }; let positions = event.positions(); let simulator = event.simulator(); let mut state = lock(&backend.world_state); for (id, position) in positions { if let Some(avatar) = state.fake.avatars.iter_mut().find(|avatar| avatar.id == id) { avatar.position = position; } else { state.fake.avatars.push(AvatarView { id, name: String::new(), position, }); } } let removed = event.removed_entries(); state .fake .avatars .retain(|avatar| !removed.contains(&avatar.id)); let follow_position = state.follow_target.and_then(|target| { state .fake .avatars .iter() .find(|avatar| avatar.id == target) .map(|avatar| avatar.position) }); drop(state); if let Some(position) = follow_position { let Ok((x, y)) = region_origin(simulator.handle) else { return; }; let _ = backend.with_agent(|agent| { agent.auto_pilot_with_double_double_double( f64::from(position.x) + f64::from(x), f64::from(position.y) + f64::from(y), f64::from(position.z), ) }); } })), ); let weak = Arc::downgrade(backend); let (objects, assets) = { let client = lock(&backend.client); (client.objects(), client.assets()) }; subscriptions.push(objects.subscribe_object_update(Arc::new(move |event| { let Some(backend) = weak.upgrade() else { return; }; let requests = { let mut state = lock(&backend.world_state); if !state.textures_enabled { return; } texture_ids(&event.prim()) .into_iter() .filter(|id| state.requested_textures.insert(*id)) .collect::>() }; for id in requests { let assets = assets.clone(); tokio::spawn(async move { let _ = assets .request_asset_with_uuid_asset_type_boolean_cancellation_token( id, AssetType::Texture, false, None, ) .await; }); } }))); } impl Backend for LiveBackend { fn world_state(&self) -> &Mutex { &self.world_state } fn world_agent_id(&self) -> UUID { self.id } fn world_snapshot<'a>( &'a self, cancellation: CancellationToken, ) -> BackendFuture<'a, Result> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("World query cancelled".into()); } let (position, simulator, grid) = { let mut client = lock(&self.client); ( client.self_().sim_position(), client.network().current_sim(), client.grid(), ) }; let simulator = simulator.ok_or("No current simulator available")?; let primitives = simulator .objects_primitives .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .values() .take(MAX_QUERY_RESULTS + 1) .cloned() .collect::>(); if primitives.len() > MAX_QUERY_RESULTS { return Err("Simulator primitive cache exceeds 65,535 entries".into()); } let parcels = simulator .parcels .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .values() .take(MAX_QUERY_RESULTS + 1) .cloned() .map(|parcel| ParcelView { parcel, owners: Vec::new(), selected: HashMap::new(), }) .collect::>(); if parcels.len() > MAX_QUERY_RESULTS { return Err("Simulator parcel cache exceeds 65,535 entries".into()); } let state = lock(&self.world_state); Ok(Snapshot { region_name: simulator.name.clone(), region_handle: simulator.handle, position, wind: simulator .wind_speeds .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(), primitives, avatars: state.fake.avatars.clone(), parcels, regions: grid.regions_read_only().into_values().collect(), layers: Vec::new(), locations: HashMap::new(), estate: None, assets: HashMap::new(), syntax: native_syntax(), }) }) } fn world_query<'a>( &'a self, query: Query, cancellation: CancellationToken, ) -> BackendFuture<'a, Result> { Box::pin(async move { match query { Query::AgentLocations(handle) => { let grid = lock(&self.client).grid(); let items = grid .map_items( handle, libremetaverse::GridItemType::AgentLocations, libremetaverse::GridLayerType::Objects, Some(cancellation.clone()), ) .await .map_err(|_| "Failed to fetch agent locations")?; let mut snapshot = self.world_snapshot(cancellation).await?; let mut values = Vec::new(); for item in items.into_iter().take(MAX_QUERY_RESULTS) { if let libremetaverse::MapItemData::AgentLocation(ref location) = item.data { values.push(AgentLocation { count: location.avatar_count, x: item.local_x(), y: item.local_y(), }); } } snapshot.locations.insert(handle, values); Ok(snapshot) } Query::FindRegion(name) => { let grid = lock(&self.client).grid(); let region = grid .get_grid_region_with_string_grid_layer_type_cancellation_token( name, libremetaverse::GridLayerType::Objects, Some(cancellation.clone()), ) .await .map_err(|_| "Simulator lookup failed")? .flatten(); let mut snapshot = self.world_snapshot(cancellation).await?; if let Some(region) = region { snapshot.regions.push(region); } Ok(snapshot) } Query::GridLayer => { let grid = lock(&self.client).grid(); let layers = Arc::new(Mutex::new(Vec::new())); let callback = Arc::clone(&layers); let subscription = grid.subscribe_grid_layer(Arc::new(move |event| { let mut values = lock(&callback); if values.len() < MAX_QUERY_RESULTS { values.push(event.layer()); } })); grid.request_map_layer( libremetaverse::GridLayerType::Objects, Some(cancellation.clone()), ) .await .map_err(|_| "Grid layer request failed")?; tokio::select! {()=tokio::time::sleep(Duration::from_millis(250))=>{},()=cancellation.cancelled()=>return Err("Grid layer request cancelled".into())} drop(subscription); let mut snapshot = self.world_snapshot(cancellation).await?; snapshot.layers.clone_from(&lock(&layers)); Ok(snapshot) } Query::GridMap => { let grid = lock(&self.client).grid(); grid.request_mainland_sims(libremetaverse::GridLayerType::Objects) .map_err(|_| "Grid map request failed")?; tokio::select! {()=tokio::time::sleep(Duration::from_millis(500))=>{},()=cancellation.cancelled()=>return Err("Grid map request cancelled".into())} self.world_snapshot(cancellation).await } Query::Parcels => { let (parcels, sim) = { let client = lock(&self.client); (client.parcels(), client.network().current_sim()) }; let sim = sim.ok_or("No current simulator available")?; parcels.request_all_sim_parcels_with_simulator_boolean_time_span_cancellation_token(sim,false,Duration::from_millis(25),Some(cancellation.clone())).await.map_err(|_|"Failed to retrieve information on all the simulator parcels")?; self.world_snapshot(cancellation).await } Query::ParcelOwners(local) => live_parcel_owners(self, local, cancellation).await, Query::ParcelObjects(local, owner) => { live_parcel_objects(self, local, owner, cancellation).await } Query::EstateCovenant(timeout) => { live_estate_covenant(self, timeout, cancellation).await } Query::DownloadTerrain(timeout) => { live_download_terrain(self, timeout, cancellation).await } } }) } fn world_mutate<'a>( &'a self, mutation: Mutation, cancellation: CancellationToken, ) -> BackendFuture<'a, Result> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("World mutation cancelled".into()); } match mutation { Mutation::Movement(movement) => live_movement(self, movement, cancellation).await, Mutation::ChangePermissions { root, permissions } => { live_change_permissions(self, root, permissions, cancellation).await } Mutation::DeRez(id) => { let (inventory, sim) = { let client = lock(&self.client); (client.inventory(), client.network().current_sim()) }; let sim = sim.ok_or("No current simulator available")?; let prim = sim .objects_primitives .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .values() .find(|prim| prim.id == id) .cloned() .ok_or_else(|| format!("Could not find object {id}"))?; let trash = inventory .find_folder_for_type_with_folder_type(FolderType::Trash) .map_err(|_| "Trash folder is unavailable")?; inventory .request_de_rez_to_inventory_with_u_int32_de_rez_destination_uuid_uuid( prim.local_id, libremetaverse::DeRezDestination::AgentInventoryTake, trash, UUID::random().map_err(|_| "Could not create transaction UUID")?, ) .map_err(|_| "DeRez request failed")?; Ok("Object removed".into()) } Mutation::Import { primitives, use_group, } => live_import(self, primitives, use_group, cancellation).await, Mutation::SetTextures(enabled) => { lock(&self.world_state).textures_enabled = enabled; if enabled { let snapshot = self.world_snapshot(cancellation.clone()).await?; let mut requests = BTreeSet::new(); for prim in &snapshot.primitives { requests.extend(texture_ids(prim)); } let assets = lock(&self.client).assets(); for id in requests { cancellation .throw_if_cancellation_requested() .map_err(|_| "Texture download cancelled")?; let should_request = lock(&self.world_state).requested_textures.insert(id); if should_request { assets .request_asset_with_uuid_asset_type_boolean_cancellation_token( id, AssetType::Texture, false, Some(cancellation.clone()), ) .await .map_err(|_| format!("Texture request failed: {id}"))?; } } } Ok("Texture setting updated".into()) } Mutation::Tree(species) => { let (objects, sim, position, group) = { let mut client = lock(&self.client); let position = client.self_().sim_position(); let group = client.self_().active_group(); ( client.objects(), client.network().current_sim(), position, group, ) }; let sim = sim.ok_or("No current simulator available")?; let position = Vector3::new_with_single_single_single( position.x, position.y, position.z + 3.0, ) .map_err(|_| "Invalid tree position")?; objects .add_tree( sim, Vector3::new_with_single_single_single(0.5, 0.5, 0.5) .map_err(|_| "Invalid tree scale")?, Quaternion::identity(), position, species, group, false, ) .map_err(|_| "Tree rez request failed")?; Ok("Tree rez requested".into()) } Mutation::UploadTerrain { name, data } => { live_upload_terrain(self, name, data, cancellation).await } } }) } fn world_texture<'a>( &'a self, id: UUID, _discard: i32, cancellation: CancellationToken, ) -> BackendFuture<'a, Result, String>> { Box::pin(async move { let assets = lock(&self.client).assets(); let texture = assets .request_asset_with_uuid_asset_type_boolean_cancellation_token( id, AssetType::Texture, true, Some(cancellation), ) .await .map_err(|_| format!("Texture request failed: {id}"))? .ok_or_else(|| format!("Download failed or texture not found: {id}"))?; Ok(texture.asset_data) }) } fn world_resolve_avatar<'a>( &'a self, name: &'a str, cancellation: CancellationToken, ) -> BackendFuture<'a, Result, String>> { Box::pin(async move { super::ClientBackend::resolve_avatar(self, name, cancellation) .await .map_err(str::to_owned) }) } } fn native_syntax() -> Vec { [ "default", "state", "state_entry", "touch_start", "timer", "integer", "float", "string", "key", "vector", "rotation", "list", "if", "else", "for", "while", "return", "jump", "TRUE", "FALSE", "NULL_KEY", "llSay", "llOwnerSay", "llParticleSystem", ] .into_iter() .map(str::to_owned) .collect() } async fn live_parcel_owners( backend: &LiveBackend, local: i32, cancellation: CancellationToken, ) -> Result { let parcels = lock(&backend.client).parcels(); let sim = backend .network .current_sim() .ok_or("No current simulator available")?; let (sender, receiver) = tokio::sync::oneshot::channel(); let sender = Arc::new(Mutex::new(Some(sender))); let callback = Arc::clone(&sender); let subscription = parcels.subscribe_parcel_object_owners_reply(Arc::new(move |event| { if let Some(sender) = lock(&callback).take() { let _ = sender.send(event.prim_owners()); } })); parcels .request_object_owners(sim, local) .map_err(|_| "Parcel owner request failed")?; let owners = tokio::select! {value=receiver=>value.map_err(|_|"Parcel owner reply channel closed")?,()=tokio::time::sleep(Duration::from_secs(10))=>return Err("Timed out waiting for packet.".into()),()=cancellation.cancelled()=>return Err("Parcel owner request cancelled".into())}; drop(subscription); let mut snapshot = backend.world_snapshot(cancellation).await?; let view=snapshot.parcels.iter_mut().find(|view|view.parcel.local_id==local).ok_or_else(||format!("Unable to find Parcel {local} in Parcels Dictionary, Did you run parcelinfo to populate the dictionary first?"))?; view.owners = owners .into_iter() .take(MAX_QUERY_RESULTS) .map(|owner| OwnerView { owner: owner.owner_id, count: owner.count, }) .collect(); Ok(snapshot) } async fn live_parcel_objects( backend: &LiveBackend, local: i32, owner: UUID, cancellation: CancellationToken, ) -> Result { let parcels = lock(&backend.client).parcels(); let (sender, receiver) = tokio::sync::oneshot::channel(); let sender = Arc::new(Mutex::new(Some(sender))); let callback = Arc::clone(&sender); let subscription = parcels.subscribe_force_select_objects_reply(Arc::new(move |event| { let ids = event.object_i_ds(); if ids.len() < 251 && let Some(sender) = lock(&callback).take() { let _ = sender.send(ids); } })); parcels .request_select_objects(local, libremetaverse::ObjectReturnType::List, owner) .map_err(|_| "Parcel object selection request failed")?; let ids = tokio::select! {value=receiver=>value.map_err(|_|"Parcel object reply channel closed")?,()=tokio::time::sleep(Duration::from_secs(30))=>return Err("Timed out waiting for packet.".into()),()=cancellation.cancelled()=>return Err("Parcel object request cancelled".into())}; drop(subscription); let mut snapshot = backend.world_snapshot(cancellation).await?; let view = snapshot .parcels .iter_mut() .find(|view| view.parcel.local_id == local) .ok_or_else(|| format!("Unable to find Parcel {local} in Parcels Dictionary"))?; view.selected.insert(owner, ids); Ok(snapshot) } async fn live_estate_covenant( backend: &LiveBackend, timeout: Duration, cancellation: CancellationToken, ) -> Result { let estate = lock(&backend.client).estate(); let (sender, receiver) = tokio::sync::oneshot::channel(); let sender = Arc::new(Mutex::new(Some(sender))); let callback = Arc::clone(&sender); let subscription = estate.subscribe_estate_covenant_reply(Arc::new(move |event| { if let Some(sender) = lock(&callback).take() { let _ = sender.send(event); } })); estate .request_covenant() .map_err(|_| "Covenant request failed")?; let reply = tokio::select! {value=receiver=>value.map_err(|_|"Covenant reply channel closed")?,()=tokio::time::sleep(timeout)=>return Err("Timeout waiting for covenant info.".into()),()=cancellation.cancelled()=>return Err("Covenant request cancelled".into())}; drop(subscription); let body = if reply.covenant_id() == UUID::zero() { String::new() } else { estate .request_covenant_notecard_with_uuid_cancellation_token( reply.covenant_id(), Some(cancellation.clone()), ) .await .map_err(|_| "Could not retrieve covenant notecard")? .map_or_else( || "Could not retrieve covenant notecard.".into(), |asset| String::from_utf8_lossy(&asset.asset_data).into_owned(), ) }; let mut snapshot = backend.world_snapshot(cancellation).await?; snapshot.estate = Some(EstateView { name: reply.estate_name(), owner: reply.estate_owner_id(), covenant: reply.covenant_id(), timestamp: u32::try_from(reply.timestamp()).unwrap_or_default(), body, terrain: Vec::new(), }); Ok(snapshot) } async fn live_download_terrain( backend: &LiveBackend, timeout: Duration, cancellation: CancellationToken, ) -> Result { let (assets, estate, region) = { let client = lock(&backend.client); ( client.assets(), client.estate(), client .network() .current_sim() .map_or_else(|| "terrain".into(), |sim| sim.name.clone()), ) }; estate.enable_live_mutations(); let (sender, receiver) = tokio::sync::oneshot::channel(); let sender = Arc::new(Mutex::new(Some(sender))); let callback = Arc::clone(&sender); let xfer_assets = assets.clone(); let initiate = assets.subscribe_initiate_download(Arc::new(move |event| { let _ = xfer_assets.request_asset_xfer( event.sim_file_name(), false, false, UUID::zero(), AssetType::Unknown, false, ); })); let xfer = assets.subscribe_xfer_received(Arc::new(move |event| { let transfer = event.xfer(); if transfer.base.success && let Some(sender) = lock(&callback).take() { let _ = sender.send(transfer.base.asset_data); } })); estate .estate_owner_message_with_string_list( "terrain".into(), vec!["download filename".into(), format!("{region}.raw")], ) .map_err(|_| "Terrain download request failed")?; let data = tokio::select! {value=receiver=>value.map_err(|_|"Terrain transfer channel closed")?,()=tokio::time::sleep(timeout)=>return Err("Timeout while waiting for terrain data".into()),()=cancellation.cancelled()=>return Err("Terrain download cancelled".into())}; drop(initiate); drop(xfer); if data.len() as u64 > MAX_FILE_BYTES { return Err("Terrain transfer exceeds the 64 MiB limit".into()); } let mut snapshot = backend.world_snapshot(cancellation).await?; snapshot.estate = Some(EstateView { name: String::new(), owner: UUID::zero(), covenant: UUID::zero(), timestamp: 0, body: String::new(), terrain: data, }); Ok(snapshot) } async fn live_movement( backend: &LiveBackend, movement: Movement, cancellation: CancellationToken, ) -> Result { match movement { Movement::Pulse(direction, duration) => { let mut agent = live_agent(backend)?; let movement = &mut agent.movement; set_direction(movement, direction, true); movement .send_update_with_boolean(Some(duration.is_zero())) .map_err(|_| "Movement update failed")?; if !duration.is_zero() { let deadline = tokio::time::Instant::now() + duration; loop { tokio::select! {()=tokio::time::sleep(Duration::from_millis(100))=>{if tokio::time::Instant::now()>=deadline{break;}movement.send_update_with_boolean(Some(false)).map_err(|_|"Movement update failed")?;},()=cancellation.cancelled()=>{set_direction(movement,direction,false);let _=movement.send_update_with_boolean(Some(true));return Err("Movement cancelled".into());}} } } set_direction(movement, direction, false); movement .send_update_with_boolean(Some(true)) .map_err(|_| "Movement stop failed")?; Ok("Movement complete".into()) } Movement::Crouch(enabled) => { live_agent(backend)? .crouch(enabled) .map_err(|_| "Crouch request failed")?; Ok("Crouch updated".into()) } Movement::Fly(enabled) => { live_agent(backend)? .fly(enabled) .map_err(|_| "Flight request failed")?; Ok("Flight updated".into()) } Movement::FlyTo(target, duration) => { live_fly_to(backend, target, duration, cancellation).await } Movement::Follow(target) => { lock(&backend.world_state).follow_target = target.as_ref().map(|value| value.0); if let Some((id, position)) = target { let snapshot = backend.world_snapshot(cancellation).await?; let (x, y) = region_origin(snapshot.region_handle)?; live_agent(backend)? .auto_pilot_with_double_double_double( f64::from(position.x) + f64::from(x), f64::from(position.y) + f64::from(y), f64::from(position.z), ) .map_err(|_| "Follow autopilot failed")?; Ok(format!("Following {id}")) } else { let _ = live_agent(backend)?.auto_pilot_cancel(); Ok("Following stopped".into()) } } Movement::GoHome => { let agent = live_agent(backend)?; let success = agent .go_home(Some(cancellation)) .await .map_err(|_| "Teleport Home Failed")?; if success { Ok("Teleport complete".into()) } else { Err("Teleport Home Failed".into()) } } Movement::TeleportRegion(name, position) => { let agent = live_agent(backend)?; let success = agent .teleport_with_string_vector3_cancellation_token(name, position, Some(cancellation)) .await .map_err(|_| "Teleport failed")?; if success { Ok("Teleport complete".into()) } else { Err(format!("Teleport failed: {}", agent.teleport_message())) } } Movement::TeleportLandmark(id) => { let agent = live_agent(backend)?; if agent .teleport_with_uuid_cancellation_token(id, Some(cancellation)) .await .map_err(|_| "Teleport Failed")? { Ok("Teleport complete".into()) } else { Err("Teleport Failed".into()) } } Movement::Jump => { live_agent(backend)? .jump(true) .map_err(|_| "Jump request failed")?; Ok("Jump complete".into()) } Movement::AutoPilot { global, .. } => { live_agent(backend)? .auto_pilot_with_double_double_double(global[0], global[1], global[2]) .map_err(|_| "Autopilot request failed")?; Ok("Autopilot started".into()) } Movement::SetHome => { live_agent(backend)? .set_home() .map_err(|_| "Set home request failed")?; Ok("Home set".into()) } Movement::Sit(id) => { let sim = backend .network .current_sim() .ok_or("No current simulator available")?; let prim = sim .objects_primitives .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .values() .find(|prim| prim.id == id) .cloned() .ok_or_else(|| format!("Could not find object {id}"))?; let agent = live_agent(backend)?; agent .request_sit(id, Vector3::zero()) .map_err(|_| "Sit request failed")?; agent.sit().map_err(|_| "Sit request failed")?; Ok(format!("Sit requested on {}", prim.local_id)) } Movement::Stand => { live_agent(backend)? .stand() .map_err(|_| "Stand request failed")?; Ok("Stand requested".into()) } Movement::Turn(target) => { let agent = live_agent(backend)?; agent .movement .turn_toward(target, Some(true)) .map_err(|_| "Turn request failed")?; Ok("Turn complete".into()) } Movement::Cross(direction, fly) => live_cross(backend, direction, fly, cancellation).await, } } fn live_agent(backend: &LiveBackend) -> Result { let client = lock(&backend.client).clone(); libremetaverse::AgentManager::new(Some(Arc::new(client))) .map_err(|_| "Could not acquire the live agent manager".into()) } fn set_direction( movement: &mut libremetaverse::AgentManagerAgentMovement, direction: MoveDirection, value: bool, ) { match direction { MoveDirection::Back => movement.set_at_neg(value), MoveDirection::Forward => movement.set_at_pos(value), MoveDirection::Left => movement.set_left_pos(value), MoveDirection::Right => movement.set_left_neg(value), } } async fn live_fly_to( backend: &LiveBackend, target: Vector3, duration: Duration, cancellation: CancellationToken, ) -> Result { let snapshot = backend.world_snapshot(cancellation.clone()).await?; let (x, y) = region_origin(snapshot.region_handle)?; let agent = live_agent(backend)?; agent .fly(true) .map_err(|_| "FlyTo could not enable flight")?; agent .auto_pilot_with_double_double_double( f64::from(target.x) + f64::from(x), f64::from(target.y) + f64::from(y), f64::from(target.z), ) .map_err(|_| "FlyTo autopilot failed")?; let deadline = tokio::time::Instant::now() + duration; loop { if distance(agent.sim_position(), target) <= 2.0 { let _ = agent.auto_pilot_cancel(); return Ok("FlyTo target reached".into()); } tokio::select! {()=tokio::time::sleep(Duration::from_millis(100))=>{if tokio::time::Instant::now()>=deadline{let _=agent.auto_pilot_cancel();return Ok("FlyTo duration elapsed".into());}},()=cancellation.cancelled()=>{let _=agent.auto_pilot_cancel();return Err("FlyTo cancelled".into());}} } } async fn live_cross( backend: &LiveBackend, direction: Vector3, fly: bool, cancellation: CancellationToken, ) -> Result { let snapshot = backend.world_snapshot(cancellation.clone()).await?; let start = snapshot.region_handle; let target_x = if direction.x > 0.0 { 266.0 } else if direction.x < 0.0 { -10.0 } else { snapshot.position.x }; let target_y = if direction.y > 0.0 { 266.0 } else if direction.y < 0.0 { -10.0 } else { snapshot.position.y }; let agent = live_agent(backend)?; agent .fly(fly) .map_err(|_| "Could not set crossing movement mode")?; let (region_x, region_y) = region_origin(start)?; agent .auto_pilot_with_double_double_double( f64::from(target_x) + f64::from(region_x), f64::from(target_y) + f64::from(region_y), f64::from(snapshot.position.z), ) .map_err(|_| "Region crossing autopilot failed")?; let deadline = tokio::time::Instant::now() + Duration::from_mins(1); loop { if backend .network .current_sim() .is_some_and(|sim| sim.handle != start) { let _ = agent.auto_pilot_cancel(); return Ok("Successfully crossed region border".into()); } tokio::select! {()=tokio::time::sleep(Duration::from_millis(100))=>{if tokio::time::Instant::now()>=deadline{let _=agent.auto_pilot_cancel();return Err("Failed to cross region border: timeout".into());}},()=cancellation.cancelled()=>{let _=agent.auto_pilot_cancel();return Err("Region crossing cancelled".into());}} } } async fn live_change_permissions( backend: &LiveBackend, root: UUID, permissions: PermissionMask, cancellation: CancellationToken, ) -> Result { let (objects, inventory, sim) = { let client = lock(&backend.client); ( client.objects(), client.inventory(), client.network().current_sim(), ) }; let sim = sim.ok_or("No current simulator available")?; let linkset = { let prims = sim .objects_primitives .read() .unwrap_or_else(std::sync::PoisonError::into_inner); let selected = prims .values() .find(|prim| prim.id == root) .cloned() .ok_or_else(|| format!("Cannot find requested object {root}"))?; let root_prim = if selected.parent_id == 0 { selected } else { prims .get(&selected.parent_id) .cloned() .ok_or("Cannot find root prim for requested object")? }; prims .values() .filter(|prim| { prim.local_id == root_prim.local_id || prim.parent_id == root_prim.local_id }) .take(MAX_EXPORT_PRIMS + 1) .cloned() .collect::>() }; if linkset.len() > MAX_EXPORT_PRIMS { return Err("Linkset exceeds the 10,000 primitive limit".into()); } let ids = linkset.iter().map(|prim| prim.local_id).collect::>(); for mask in [ PermissionMask::MODIFY, PermissionMask::COPY, PermissionMask::TRANSFER, ] { cancellation .throw_if_cancellation_requested() .map_err(|_| "Permission update cancelled")?; objects .set_permissions( sim.clone(), ids.clone(), libremetaverse::PermissionWho::NEXT_OWNER, mask, permissions.0 & mask.0 != 0, ) .map_err(|_| "Failed to set linkset permissions")?; tokio::select! {()=tokio::time::sleep(Duration::from_millis(250))=>{},()=cancellation.cancelled()=>return Err("Permission update cancelled".into())} } let mut task_items = 0; for prim in &linkset { if prim.flags.0 & PrimFlags::INVENTORY_EMPTY.0 != 0 { continue; } let items = inventory .get_task_inventory( prim.id, prim.local_id, Some(sim.clone()), Some(cancellation.clone()), ) .await .map_err(|_| "Task inventory request failed")?; for entry in items { if let Some(mut item) = entry .as_any() .downcast_ref::() .cloned() { let mut perms = item.permissions(); perms.next_owner_mask = permissions; item.set_permissions(perms); inventory .update_task_inventory(prim.local_id, item, Some(sim.clone()), Some(true)) .map_err(|_| "Task inventory permission update failed")?; task_items += 1; } } } Ok(format!( "Set permissions to {:?} on {} objects and {task_items} inventory items", permissions, ids.len() )) } async fn next_created( receiver: &mut tokio::sync::mpsc::UnboundedReceiver, cancellation: &CancellationToken, ) -> Result { tokio::select! {value=receiver.recv()=>value.ok_or_else(||"Primitive creation event channel closed".into()),()=tokio::time::sleep(Duration::from_secs(10))=>Err("Rez failed, timed out while creating prim.".into()),()=cancellation.cancelled()=>Err("Import cancelled".into())} } fn apply_prim_properties( objects: &libremetaverse::ObjectManager, sim: &libremetaverse::Simulator, created: &Primitive, source: &Primitive, position: Vector3, ) -> Result<(), String> { objects .set_position_with_simulator_u_int32_vector3(sim.clone(), created.local_id, position) .map_err(|_| "Setting imported primitive position failed")?; if let Some(textures) = &source.textures { objects .set_textures_with_simulator_u_int32_texture_entry( sim.clone(), created.local_id, textures.clone(), ) .map_err(|_| "Setting imported primitive textures failed")?; } if let Some(light) = &source.light { objects .set_light(sim.clone(), created.local_id, light.clone()) .map_err(|_| "Setting imported primitive light failed")?; } if let Some(flexible) = &source.flexible { objects .set_flexible(sim.clone(), created.local_id, flexible.clone()) .map_err(|_| "Setting imported primitive flexibility failed")?; } if let Some(sculpt) = &source.sculpt { objects .set_sculpt(sim.clone(), created.local_id, sculpt.clone()) .map_err(|_| "Setting imported primitive sculpt failed")?; } if let Some(properties) = &source.properties { if !properties.name.is_empty() { objects .set_name(sim.clone(), created.local_id, properties.name.clone()) .map_err(|_| "Setting imported primitive name failed")?; } if !properties.description.is_empty() { objects .set_description( sim.clone(), created.local_id, properties.description.clone(), ) .map_err(|_| "Setting imported primitive description failed")?; } } Ok(()) } async fn live_import( backend: &LiveBackend, primitives: Vec, use_group: bool, cancellation: CancellationToken, ) -> Result { let (objects, sim, agent_position, group) = { let mut client = lock(&backend.client); let objects = client.objects(); let sim = client.network().current_sim(); let agent = client.self_(); ( objects, sim, agent.sim_position(), if use_group { agent.active_group() } else { UUID::zero() }, ) }; let sim = sim.ok_or("No current simulator available")?; let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); let subscription = objects.subscribe_object_update(Arc::new(move |event| { if event.is_new() && event.prim().flags.0 & PrimFlags::CREATE_SELECTED.0 != 0 { let _ = sender.send(event.prim()); } })); let mut roots: Vec<_> = primitives .iter() .filter(|prim| prim.parent_id == 0) .collect(); roots.sort_by_key(|prim| prim.local_id); if roots.is_empty() { return Err("Import contains no root primitives".into()); } let mut created_count = 0; for root in roots { let base = Vector3::new_with_single_single_single( agent_position.x, agent_position.y, agent_position.z + 3.0, ) .map_err(|_| "Invalid import position")?; objects .add_prim_with_simulator_construction_data_uuid_vector3_vector3_quaternion( sim.clone(), root.prim_data.clone(), group, base, root.scale, Quaternion::identity(), ) .map_err(|_| "Rez request failed for root primitive")?; let created_root = next_created(&mut receiver, &cancellation).await?; apply_prim_properties(&objects, &sim, &created_root, root, base)?; created_count += 1; let mut ids = vec![created_root.local_id]; let mut children: Vec<_> = primitives .iter() .filter(|prim| prim.parent_id == root.local_id) .collect(); children.sort_by_key(|prim| prim.local_id); for child in children { let position = Vector3::new_with_single_single_single( base.x + child.position.x, base.y + child.position.y, base.z + child.position.z, ) .map_err(|_| "Invalid child import position")?; objects .add_prim_with_simulator_construction_data_uuid_vector3_vector3_quaternion( sim.clone(), child.prim_data.clone(), group, position, child.scale, child.rotation, ) .map_err(|_| "Rez request failed for child primitive")?; let created = next_created(&mut receiver, &cancellation).await?; apply_prim_properties(&objects, &sim, &created, child, position)?; ids.push(created.local_id); created_count += 1; } if ids.len() > 1 { objects .link_prims(sim.clone(), ids.clone()) .map_err(|_| "Linking imported primitives failed")?; } objects .set_rotation_with_simulator_u_int32_quaternion( sim.clone(), created_root.local_id, root.rotation, ) .map_err(|_| "Setting imported root rotation failed")?; objects .set_permissions( sim.clone(), ids, libremetaverse::PermissionWho::ALL, PermissionMask::ALL, true, ) .map_err(|_| "Setting imported permissions failed")?; } drop(subscription); Ok(format!("Imported {created_count} primitives")) } async fn live_upload_terrain( backend: &LiveBackend, name: String, data: Vec, cancellation: CancellationToken, ) -> Result { let (estate, assets) = { let client = lock(&backend.client); (client.estate(), client.assets()) }; estate.enable_live_mutations(); let (sender, receiver) = tokio::sync::oneshot::channel(); let sender = Arc::new(Mutex::new(Some(sender))); let callback = Arc::clone(&sender); let subscription = assets.subscribe_upload_progress(Arc::new(move |event| { let upload = event.upload(); if upload.base.transferred == upload.base.size && let Some(sender) = lock(&callback).take() { let _ = sender.send(upload.base.success); } })); estate .upload_terrain(data, name) .map_err(|_| "Terrain upload request failed")?; let success = tokio::select! {value=receiver=>value.map_err(|_|"Terrain upload channel closed")?,()=tokio::time::sleep(Duration::from_mins(2))=>return Err("Timeout waiting for terrain file upload".into()),()=cancellation.cancelled()=>return Err("Terrain upload cancelled".into())}; drop(subscription); if success { Ok("Terrain upload completed".into()) } else { Err("Terrain upload failed".into()) } }