Files
MetaCrate/programs/src/test_client/world.rs
Chili Palmer 25fdffbd3b
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m51s
CI / required (push) Failing after 3m23s
feat(imaging): default to pure-Rust codecs
2026-08-13 11:24:38 +00:00

3662 lines
129 KiB
Rust

//! 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<super::CommandEntry> {
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<OwnerView>,
selected: HashMap<UUID, Vec<u32>>,
}
#[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<u8>,
}
#[derive(Clone, Debug, Default)]
struct Snapshot {
region_name: String,
region_handle: u64,
position: Vector3,
wind: Option<Vec<Vector2>>,
primitives: Vec<Primitive>,
avatars: Vec<AvatarView>,
parcels: Vec<ParcelView>,
regions: Vec<GridRegion>,
layers: Vec<GridLayer>,
locations: HashMap<u64, Vec<AgentLocation>>,
estate: Option<EstateView>,
assets: HashMap<UUID, Vec<u8>>,
syntax: Vec<String>,
}
pub(super) struct State {
policy: super::inventory::MutationPolicy,
fake: Snapshot,
textures_enabled: bool,
requested_textures: HashSet<UUID>,
follow_target: Option<UUID>,
}
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<Primitive>,
use_group: bool,
},
SetTextures(bool),
Tree(Tree),
UploadTerrain {
name: String,
data: Vec<u8>,
},
}
#[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<State>;
fn world_agent_id(&self) -> UUID;
fn world_snapshot<'a>(
&'a self,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<Snapshot, String>>;
fn world_query<'a>(
&'a self,
query: Query,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<Snapshot, String>>;
fn world_mutate<'a>(
&'a self,
mutation: Mutation,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<String, String>>;
fn world_texture<'a>(
&'a self,
id: UUID,
discard: i32,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<Vec<u8>, String>>;
fn world_resolve_avatar<'a>(
&'a self,
name: &'a str,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<Option<UUID>, String>>;
fn apply_world_fixture(&self, _fields: &[&str]) -> Result<(), &'static str> {
Err("world fixture requires fake-grid mode")
}
}
pub(super) async fn execute<B: Backend + ?Sized>(
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
spending: bool,
estate: bool,
) -> Result<Vec<String>, 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, String> {
UUID::new_with_string(value.into()).map_err(|_| format!("{value} is not a valid UUID"))
}
fn parse_vector(args: &[String], usage: &str) -> Result<Vector3, String> {
if args.len() != 3 {
return Err(usage.into());
}
let values = args
.iter()
.map(|value| value.parse::<f32>())
.collect::<Result<Vec<_>, _>>()
.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<PathBuf, String> {
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<Vec<u8>, 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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
direction: MoveDirection,
cancellation: CancellationToken,
) -> Result<String, String> {
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::<u64>())
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
crouch: bool,
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
movement: Movement,
output: &str,
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<u64>())
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<Vec<_>>(),
"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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
explicit: Option<()>,
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<u64>())
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
if args.len() != 1 {
return Err("Usage: parceldetails parcelID (use parcelinfo to get ID)".into());
}
let id = args[0]
.parse::<i32>()
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
if args.len() != 1 {
return Err("Usage: primowners parcelID (use parcelinfo to get ID)".into());
}
let id = args[0]
.parse::<i32>()
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<i32>().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::<Vec<_>>()
.join(" ");
if !output.is_empty() {
output.push(' ');
}
output.push_str(&format!("Found a total of {} Objects", ids.len()));
Ok(output)
}
async fn syntax_id<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
if args.len() > 1 {
return Err("Usage: getestatecovenant [timeout]".into());
}
let seconds = args
.first()
.map_or(Ok(20), |v| v.parse::<u64>())
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<u64>())
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<i32>())
.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<UUID> {
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<Vec<&Primitive>, 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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<Result<Vec<_>, _>>()
.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<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
if args.is_empty() || args.len() > 2 {
return Err("Usage: findobjects [radius] <search-string>".into());
}
let radius = args[0]
.parse::<f32>()
.map_err(|_| "Usage: findobjects [radius] <search-string>")?;
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
if args.len() != 2 {
return Err("Usage: findtexture [face-index] [texture-uuid]".into());
}
let face = args[0]
.parse::<usize>()
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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::<Result<Vec<_>, _>>()
.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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<Tree> {
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<B: Backend + ?Sized>(
backend: &B,
args: &[String],
cancellation: CancellationToken,
) -> Result<String, String> {
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<State> {
&self.world_state
}
fn world_agent_id(&self) -> UUID {
self.id
}
fn world_snapshot<'a>(
&'a self,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<Snapshot, String>> {
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<Snapshot, String>> {
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<String, String>> {
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::<HashMap<_, _>>();
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<Vec<u8>, 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<Option<UUID>, 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<String, String> {
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<Result<(), &'static str>> {
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, &'static str> {
UUID::new_with_string(value.into()).map_err(|_| "world fixture UUID is invalid")
}
fn fixture_number<T: std::str::FromStr>(value: &str) -> Result<T, &'static str> {
value.parse().map_err(|_| "world fixture number is invalid")
}
fn fixture_vector(x: &str, y: &str, z: &str) -> Result<Vector3, &'static str> {
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<Vec<u8>, &'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<u8> {
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::<Result<_, _>>()?
};
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<LiveBackend>, subscriptions: &mut Vec<Subscription>) {
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::<Vec<_>>()
};
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<State> {
&self.world_state
}
fn world_agent_id(&self) -> UUID {
self.id
}
fn world_snapshot<'a>(
&'a self,
cancellation: CancellationToken,
) -> BackendFuture<'a, Result<Snapshot, String>> {
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::<Vec<_>>();
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::<Vec<_>>();
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<Snapshot, String>> {
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<String, String>> {
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<Vec<u8>, 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<Option<UUID>, String>> {
Box::pin(async move {
super::ClientBackend::resolve_avatar(self, name, cancellation)
.await
.map_err(str::to_owned)
})
}
}
fn native_syntax() -> Vec<String> {
[
"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<Snapshot, String> {
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<Snapshot, String> {
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<Snapshot, String> {
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<Snapshot, String> {
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<String, String> {
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<libremetaverse::AgentManager, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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::<Vec<_>>()
};
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::<Vec<_>>();
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::<libremetaverse::InventoryItem>()
.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<Primitive>,
cancellation: &CancellationToken,
) -> Result<Primitive, String> {
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<Primitive>,
use_group: bool,
cancellation: CancellationToken,
) -> Result<String, String> {
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<u8>,
cancellation: CancellationToken,
) -> Result<String, String> {
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())
}
}