//! Estate administration decisions, owner-message payloads, and capabilities. #![allow(clippy::missing_errors_doc)] #![allow(clippy::must_use_candidate)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::similar_names)] #![allow(clippy::too_many_arguments)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::fn_params_excessive_bools)] #![allow(clippy::unnecessary_wraps)] use crate::agent_manager::EventRegistry; use crate::packet_catalog::{GeneratedPacket, PacketType}; use crate::{ Error, EstateToolsEstateAccessDelta as AccessDelta, EstateToolsEstateReturnFlags, EstateToolsGroundTextureHeight, EstateToolsGroundTextureHeightSettings, EstateToolsGroundTextureSettings, EstateToolsLandStatReportType, EstateToolsRegionMaturity, GridClient, RegionFlags, RegionRestartDays, SourceType, }; use futures_channel::oneshot; use futures_util::{FutureExt, pin_mut, select_biased}; use libremetaverse_structured_data::{OSD, OSDParser}; use libremetaverse_types::AssetType; use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri}; use libremetaverse_types::{UUID, Vector3}; use serde_json::Value; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; fn blank_textures() -> EstateToolsGroundTextureSettings { EstateToolsGroundTextureSettings { low: UUID::zero(), mid_low: UUID::zero(), mid_high: UUID::zero(), high: UUID::zero(), } } fn blank_limits() -> EstateToolsGroundTextureHeightSettings { let height = || EstateToolsGroundTextureHeight { low: 0.0, high: 0.0, }; EstateToolsGroundTextureHeightSettings { sw: height(), nw: height(), se: height(), ne: height(), } } #[derive(Clone, Debug)] pub struct RegionRestartSchedule { daily: bool, days: RegionRestartDays, time: Duration, } impl RegionRestartSchedule { pub fn new() -> Result { Ok(Self { daily: false, days: RegionRestartDays::NONE, time: Duration::ZERO, }) } pub const fn is_daily(&self) -> bool { self.daily } pub fn set_is_daily(&mut self, value: bool) { self.daily = value; } pub const fn days(&self) -> RegionRestartDays { self.days } pub fn set_days(&mut self, value: RegionRestartDays) { self.days = value; } pub const fn time(&self) -> Duration { self.time } pub fn set_time(&mut self, value: Duration) { self.time = value; } } #[derive(Clone, Debug, Default)] pub struct EstateTask { pub position: Vector3, pub score: f32, pub mono_score: f32, pub task_id: UUID, pub task_local_id: u32, pub task_name: String, pub owner_name: String, } impl EstateTask { pub fn new() -> Result { Ok(Self::default()) } } macro_rules! list_args { ($name:ident, $field:ident, $getter:ident) => { #[derive(Clone, Debug)] pub struct $name { estate_id: u32, count: i32, $field: Vec, } impl $name { pub fn new(estate_id: u32, count: i32, $field: Vec) -> Result { Ok(Self { estate_id, count, $field, }) } pub const fn estate_id(&self) -> u32 { self.estate_id } pub const fn count(&self) -> i32 { self.count } pub fn $getter(&self) -> Vec { self.$field.clone() } } }; } list_args!(EstateBansReplyEventArgs, banned, banned); list_args!(EstateGroupsReplyEventArgs, allowed_groups, allowed_groups); list_args!(EstateManagersReplyEventArgs, managers, managers); list_args!(EstateUsersReplyEventArgs, allowed_users, allowed_users); #[derive(Clone, Debug)] pub struct EstateExperienceReplyEventArgs { blocked: Vec, trusted: Vec, allowed: Vec, } impl EstateExperienceReplyEventArgs { pub fn new(blocked: Vec, trusted: Vec, allowed: Vec) -> Result { Ok(Self { blocked, trusted, allowed, }) } pub fn blocked(&self) -> Vec { self.blocked.clone() } pub fn trusted(&self) -> Vec { self.trusted.clone() } pub fn allowed(&self) -> Vec { self.allowed.clone() } } #[derive(Clone, Debug)] pub struct EstateCovenantReplyEventArgs { covenant_id: UUID, timestamp: i64, estate_name: String, estate_owner_id: UUID, } impl EstateCovenantReplyEventArgs { pub fn new( covenant_id: UUID, timestamp: i64, estate_name: String, estate_owner_id: UUID, ) -> Result { Ok(Self { covenant_id, timestamp, estate_name, estate_owner_id, }) } pub const fn covenant_id(&self) -> UUID { self.covenant_id } pub const fn timestamp(&self) -> i64 { self.timestamp } pub fn estate_name(&self) -> String { self.estate_name.clone() } pub const fn estate_owner_id(&self) -> UUID { self.estate_owner_id } } #[derive(Clone, Debug)] pub struct EstateUpdateInfoReplyEventArgs { estate_name: String, estate_owner: UUID, estate_id: u32, flags: RegionFlags, sun_hour: i32, } impl EstateUpdateInfoReplyEventArgs { pub fn new( estate_name: String, estate_owner: UUID, estate_id: u32, flags: RegionFlags, sun_hour: i32, ) -> Result { Ok(Self { estate_name, estate_owner, estate_id, flags, sun_hour, }) } pub fn estate_name(&self) -> String { self.estate_name.clone() } pub const fn estate_owner(&self) -> UUID { self.estate_owner } pub const fn estate_id(&self) -> u32 { self.estate_id } pub const fn flags(&self) -> RegionFlags { self.flags } pub const fn sun_hour(&self) -> i32 { self.sun_hour } } macro_rules! task_args { ($name:ident) => { #[derive(Clone, Debug)] pub struct $name { object_count: i32, tasks: HashMap, } impl $name { pub fn new(object_count: i32, tasks: HashMap) -> Result { Ok(Self { object_count, tasks, }) } pub const fn object_count(&self) -> i32 { self.object_count } pub fn tasks(&self) -> HashMap { self.tasks.clone() } } }; } task_args!(TopCollidersReplyEventArgs); task_args!(TopScriptsReplyEventArgs); #[derive(Default)] struct EstateEvents { bans: EventRegistry, covenant: EventRegistry, experiences: EventRegistry, groups: EventRegistry, managers: EventRegistry, update: EventRegistry, users: EventRegistry, colliders: EventRegistry, scripts: EventRegistry, } pub(crate) struct EstateToolsInner { client: GridClient, network: crate::NetworkManager, events: EstateEvents, disposed: AtomicBool, live_mutations: AtomicBool, sim_console_lock: tokio::sync::Mutex<()>, subscriptions: Mutex>, } pub struct EstateTools { pub ground_texture_limits: EstateToolsGroundTextureHeightSettings, pub ground_textures: EstateToolsGroundTextureSettings, pub(crate) inner: Arc, } impl Clone for EstateTools { fn clone(&self) -> Self { Self { ground_texture_limits: EstateToolsGroundTextureHeightSettings { sw: EstateToolsGroundTextureHeight { low: self.ground_texture_limits.sw.low, high: self.ground_texture_limits.sw.high, }, nw: EstateToolsGroundTextureHeight { low: self.ground_texture_limits.nw.low, high: self.ground_texture_limits.nw.high, }, se: EstateToolsGroundTextureHeight { low: self.ground_texture_limits.se.low, high: self.ground_texture_limits.se.high, }, ne: EstateToolsGroundTextureHeight { low: self.ground_texture_limits.ne.low, high: self.ground_texture_limits.ne.high, }, }, ground_textures: EstateToolsGroundTextureSettings { low: self.ground_textures.low, mid_low: self.ground_textures.mid_low, mid_high: self.ground_textures.mid_high, high: self.ground_textures.high, }, inner: Arc::clone(&self.inner), } } } impl EstateTools { pub(crate) fn native_new(client: GridClient) -> Result { let network = client.network(); let candidate = Arc::new(EstateToolsInner { client: client.clone(), network: network.clone(), events: EstateEvents::default(), disposed: AtomicBool::new(false), live_mutations: AtomicBool::new(false), sim_console_lock: tokio::sync::Mutex::new(()), subscriptions: Mutex::new(Vec::new()), }); let weak = Arc::downgrade(&candidate); candidate .subscriptions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(network.subscribe_raw_packet(Arc::new(move |event| { if let Some(inner) = weak.upgrade() { inner.handle_packet(event); } }))); let weak = Arc::downgrade(&candidate); candidate .subscriptions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(network.subscribe_caps( "LandStatReply".into(), crate::CapsEventQueueCallback::from_handler(move |_name, message, _simulator| { let Some(inner) = weak.upgrade() else { return; }; let Ok(mut reply) = crate::messages::linden::LandStatReplyMessage::new() else { return; }; if message .serialize() .and_then(|map| reply.deserialize(map)) .is_ok() { inner.handle_land_stat_caps(reply); } }), )); let inner = client.install_estate_tools_inner(candidate); Ok(Self::native_from_inner(inner)) } pub(crate) fn native_from_inner(inner: Arc) -> Self { Self { ground_texture_limits: blank_limits(), ground_textures: blank_textures(), inner, } } pub(crate) fn native_inner(&self) -> Arc { Arc::clone(&self.inner) } pub fn new(client: GridClient) -> Result { Self::native_new(client) } pub fn enable_live_mutations(&self) { self.inner.live_mutations.store(true, Ordering::Release); } pub fn dispose(&self) -> Result<(), Error> { self.inner.disposed.store(true, Ordering::Release); self.inner .subscriptions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); Ok(()) } pub fn subscribe_estate_bans_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.bans.subscribe(h) } pub fn subscribe_estate_covenant_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.covenant.subscribe(h) } pub fn subscribe_estate_experience_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.experiences.subscribe(h) } pub fn subscribe_estate_groups_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.groups.subscribe(h) } pub fn subscribe_estate_managers_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.managers.subscribe(h) } pub fn subscribe_estate_update_info_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.update.subscribe(h) } pub fn subscribe_estate_users_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.users.subscribe(h) } pub fn subscribe_top_colliders_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.colliders.subscribe(h) } pub fn subscribe_top_scripts_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.scripts.subscribe(h) } #[allow(dead_code)] pub(crate) fn estate_owner_message_handler( &self, packet: crate::packets::EstateOwnerMessagePacket, _simulator: crate::Simulator, ) -> Result<(), Error> { if self.inner.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } self.inner.handle_owner_message(packet); Ok(()) } fn ids(&self) -> (UUID, UUID, String) { let mut client = self.inner.client.clone(); let agent = client.self_(); (agent.agent_id(), agent.session_id(), agent.name()) } fn current_sim(&self) -> Result { self.inner .client .native_network()? .native_current_sim() .ok_or(Error::InvalidOperation) } fn ensure_udp_mutation_allowed(&self) -> Result<(), Error> { let sim = self.current_sim()?; if sim.connected() && !self.inner.live_mutations.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } Ok(()) } fn send_packet(&self, packet: &T, kind: PacketType) -> Result<(), Error> { if self.inner.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let sim = self.current_sim()?; let bytes = packet.encode_packet()?; let result = sim.native_send_packet_data( bytes.clone(), i32::try_from(bytes.len()).map_err(|_| Error::Argument)?, kind, false, ); match result { Err(Error::InvalidOperation) if !sim.connected() => Ok(()), other => other, } } pub fn estate_owner_message_with_string_list( &self, method: String, params: Vec, ) -> Result<(), Error> { self.ensure_udp_mutation_allowed()?; if method.len() > 255 || params.len() > 255 || params.iter().any(|value| value.len() > 255) { return Err(Error::Argument); } let (agent, session, _) = self.ids(); let mut packet = crate::packets::EstateOwnerMessagePacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.transaction_id = UUID::zero(); packet.method_data.invoice = UUID::random()?; packet.method_data.method = method.into_bytes(); for value in params { let mut block = crate::packets::EstateOwnerMessagePacketParamListBlock::new_with_constructor()?; block.parameter = value.into_bytes(); packet.param_list.push(block); } self.send_packet(&packet, PacketType::EstateOwnerMessage) } pub fn estate_owner_message_with_string_string( &self, method: String, param: String, ) -> Result<(), Error> { self.estate_owner_message_with_string_list(method, vec![param]) } fn owner_message(&self, method: &str, params: Vec) -> Result<(), Error> { self.estate_owner_message_with_string_list(method.into(), params) } fn access_delta( &self, target: UUID, local: AccessDelta, all: AccessDelta, all_estates: bool, ) -> Result<(), Error> { let (agent, _, _) = self.ids(); self.owner_message( "estateaccessdelta", vec![ agent.to_string(), (if all_estates { all as u32 } else { local as u32 }) .to_string(), target.to_string(), ], ) } pub fn ban_user(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::BanUser, AccessDelta::BanUserAllEstates, all, ) } pub fn unban_user(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::UnbanUser, AccessDelta::UnbanUserAllEstates, all, ) } pub fn add_estate_manager(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::AddManager, AccessDelta::AddManagerAllEstates, all, ) } pub fn remove_estate_manager(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::RemoveManager, AccessDelta::RemoveManagerAllEstates, all, ) } pub fn add_allowed_user(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::AddUserAsAllowed, AccessDelta::AddAllowedAllEstates, all, ) } pub fn remove_allowed_user(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::RemoveUserAsAllowed, AccessDelta::RemoveUserAllowedAllEstates, all, ) } pub fn add_allowed_group(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::AddGroupAsAllowed, AccessDelta::AddGroupAllowedAllEstates, all, ) } pub fn remove_allowed_group(&self, id: UUID, all: bool) -> Result<(), Error> { self.access_delta( id, AccessDelta::RemoveGroupAsAllowed, AccessDelta::RemoveGroupAllowedAllEstates, all, ) } pub fn request_info(&self) -> Result<(), Error> { self.owner_message("getinfo", vec![String::new()]) } pub fn kick_user(&self, id: UUID) -> Result<(), Error> { self.owner_message("kickestate", vec![id.to_string()]) } pub fn restart_region_with_method(&self) -> Result<(), Error> { self.restart_region_with_int32(120) } pub fn restart_region_with_int32(&self, delay: i32) -> Result<(), Error> { self.owner_message("restart", vec![delay.clamp(30, 240).to_string()]) } pub fn cancel_restart(&self) -> Result<(), Error> { self.owner_message("restart", vec!["-1".into()]) } pub fn estate_message(&self, message: String) -> Result<(), Error> { let (_, _, name) = self.ids(); self.owner_message("instantmessage", vec![name, message]) } pub fn simulator_message(&self, message: String) -> Result<(), Error> { let (agent, _, name) = self.ids(); self.owner_message( "simulatormessage", vec!["-1".into(), "-1".into(), agent.to_string(), name, message], ) } pub fn teleport_home_user(&self, id: UUID) -> Result<(), Error> { let (agent, _, _) = self.ids(); self.owner_message("teleporthomeuser", vec![agent.to_string(), id.to_string()]) } pub fn teleport_home_all_users(&self) -> Result<(), Error> { let (agent, _, _) = self.ids(); self.owner_message("teleporthomeallusers", vec![agent.to_string()]) } pub fn request_top_scripts(&self) -> Result<(), Error> { self.land_stat_request( 0, EstateToolsLandStatReportType::TopScripts, 0, String::new(), ) } pub fn request_top_colliders(&self) -> Result<(), Error> { self.land_stat_request( 0, EstateToolsLandStatReportType::TopColliders, 0, String::new(), ) } pub fn set_region_debug( &self, scripts: bool, collisions: bool, physics: bool, ) -> Result<(), Error> { self.owner_message( "setregiondebug", vec![yn(scripts), yn(collisions), yn(physics)], ) } pub fn set_region_info_udp( &self, terraform: bool, fly: bool, damage: bool, resell: bool, pushing: bool, join: bool, agents: f32, bonus: f32, maturity: EstateToolsRegionMaturity, ) -> Result<(), Error> { self.owner_message( "setregioninfo", vec![ yn(terraform), yn(fly), yn(damage), yn(resell), agents.to_string(), bonus.to_string(), (maturity as i32).to_string(), yn(pushing), yn(join), ], ) } pub fn set_terrain_variables( &self, water: f32, raise: f32, lower: f32, estate_sun: bool, fixed_sun: bool, sun: f32, ) -> Result<(), Error> { self.owner_message( "setregionterrain", vec![ water.to_string(), raise.to_string(), lower.to_string(), yn(estate_sun), yn(fixed_sun), sun.to_string(), "Y".into(), "N".into(), "0.00".into(), ], ) } pub fn set_region_terrain( &self, low: UUID, mid_low: UUID, mid_high: UUID, high: UUID, ) -> Result<(), Error> { self.owner_message( "texturedetail", vec![ format!("0 {low}"), format!("1 {mid_low}"), format!("2 {mid_high}"), format!("3 {high}"), ], )?; self.owner_message("texturecommit", vec![String::new()]) } pub fn set_region_terrain_heights( &self, low_sw: f32, high_sw: f32, low_nw: f32, high_nw: f32, low_se: f32, high_se: f32, low_ne: f32, high_ne: f32, ) -> Result<(), Error> { self.owner_message( "textureheights", vec![ format!("0 {low_sw} {high_sw}"), format!("1 {low_nw} {high_nw}"), format!("2 {low_se} {high_se}"), format!("3 {low_ne} {high_ne}"), ], )?; self.owner_message("texturecommit", vec![String::new()]) } pub fn land_stat_request( &self, parcel: i32, report: EstateToolsLandStatReportType, flags: u32, filter: String, ) -> Result<(), Error> { let (agent, session, _) = self.ids(); let mut packet = crate::packets::LandStatRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.request_data.filter = filter.into_bytes(); packet.request_data.parcel_local_id = parcel; packet.request_data.report_type = report as u32; packet.request_data.request_flags = flags; self.send_packet(&packet, PacketType::LandStatRequest) } pub fn request_covenant(&self) -> Result<(), Error> { let (agent, session, _) = self.ids(); let mut packet = crate::packets::EstateCovenantRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; self.send_packet(&packet, PacketType::EstateCovenantRequest) } pub fn sim_wide_return( &self, target: UUID, flag: EstateToolsEstateReturnFlags, estate_wide: bool, ) -> Result<(), Error> { if estate_wide { return self.owner_message( "estateobjectreturn", vec![flag.0.to_string(), target.to_string()], ); } self.ensure_udp_mutation_allowed()?; let (agent, session, _) = self.ids(); let mut packet = crate::packets::SimWideDeletesPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.data_block.target_id = target; packet.data_block.flags = flag.0; self.send_packet(&packet, PacketType::SimWideDeletes) } async fn capability( &self, name: &str, token: &CancellationToken, ) -> Result, Error> { token.throw_if_cancellation_requested()?; let sim = self.current_sim()?; if let Some(caps) = sim.native_caps() && let Some(uri) = caps.capability_uri(name.into())? { return Ok(Some(uri)); } let Some(seed) = sim.seed_capability() else { return Ok(None); }; let client = sim.native_data_arc().client.clone(); let (response, body) = client .native_http_caps_client() .get(seed, token.clone(), None) .await?; if !(200..300).contains(&response.status_code) { return Ok(None); } let OSD::Map(map) = OSDParser::deserialize_with_bytes(body)? else { return Ok(None); }; Ok(map .get(name) .and_then(|value| value.as_uri().ok().flatten())) } pub async fn get_region_restart_schedule( &self, ct: Option, ) -> Result, Error> { let token = ct.unwrap_or_default(); let Some(uri) = self.capability("RegionSchedule", &token).await? else { return Ok(None); }; let (response, body) = self .inner .client .native_http_caps_client() .get(uri, token, None) .await?; if !(200..300).contains(&response.status_code) { return Ok(None); } let root: Value = match serde_json::from_slice(&body) { Ok(value) => value, Err(_) => return Ok(None), }; let Some(restart) = root.get("restart") else { return Ok(None); }; let daily = restart.get("type").and_then(Value::as_str) != Some("W"); let days = if daily { RegionRestartDays::ALL } else { letters_to_days( restart .get("days") .and_then(Value::as_str) .unwrap_or_default(), ) }; let seconds = restart .get("time") .and_then(Value::as_u64) .unwrap_or_default(); Ok(Some(RegionRestartSchedule { daily, days, time: Duration::from_secs(seconds), })) } pub async fn set_region_restart_schedule( &self, schedule: RegionRestartSchedule, ct: Option, ) -> Result { let token = ct.unwrap_or_default(); let Some(uri) = self.capability("RegionSchedule", &token).await? else { return Ok(false); }; if !crate::is_offline_fixture_uri(&uri) && !self.inner.live_mutations.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let mut restart = HashMap::from([ ( "type".into(), OSD::String(if schedule.daily { "D" } else { "W" }.into()), ), ( "time".into(), OSD::Integer(i32::try_from(schedule.time.as_secs()).unwrap_or(i32::MAX)), ), ]); if !schedule.daily { restart.insert("days".into(), OSD::String(days_to_letters(schedule.days))); } let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([( "restart".into(), OSD::Map(restart), )])))?; let (response, _) = self .inner .client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".into(), body, token, None, ) .await?; Ok((200..300).contains(&response.status_code)) } pub async fn request_covenant_notecard_with_uuid_cancellation_token( &self, covenant_id: UUID, ct: Option, ) -> Result, Error> { let simulator = match self.current_sim() { Ok(simulator) => simulator, Err(Error::InvalidOperation) => return Ok(None), Err(error) => return Err(error), }; self.request_covenant_notecard_with_uuid_simulator_cancellation_token( covenant_id, simulator, ct, ) .await } pub async fn request_covenant_notecard_with_uuid_simulator_cancellation_token( &self, covenant_id: UUID, _simulator: crate::Simulator, ct: Option, ) -> Result, Error> { self.inner .client .native_assets()? .request_asset_with_uuid_asset_type_boolean_source_type_cancellation_token( covenant_id, AssetType::Notecard, true, SourceType::SimEstate, ct, ) .await } pub fn send_estate_change_info_dataserver( &self, estate_name: String, sun_hour: f32, flags: RegionFlags, ) -> Result<(), Error> { self.owner_message( "estatechangeinfo", vec![ estate_name, flags.0.to_string(), ((sun_hour * 1024.0) as i32).to_string(), ], ) } pub async fn send_estate_change_info( &self, estate_name: String, sun_hour: f32, flags: RegionFlags, ct: Option, ) -> Result<(), Error> { let token = ct.unwrap_or_default(); if let Some(uri) = self.capability("EstateChangeInfo", &token).await? { if !crate::is_offline_fixture_uri(&uri) && !self.inner.live_mutations.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let has = |flag: RegionFlags| flags.0 & flag.0 != 0; let payload = OSD::Map(HashMap::from([ ("estate_name".into(), OSD::String(estate_name.clone())), ( "is_sun_fixed".into(), OSD::Boolean(has(RegionFlags::SUN_FIXED)), ), ( "is_externally_visible".into(), OSD::Boolean(has(RegionFlags::EXTERNALLY_VISIBLE)), ), ( "allow_direct_teleport".into(), OSD::Boolean(has(RegionFlags::ALLOW_DIRECT_TELEPORT)), ), ( "deny_anonymous".into(), OSD::Boolean(has(RegionFlags::DENY_ANONYMOUS)), ), ( "deny_age_unverified".into(), OSD::Boolean(has(RegionFlags::DENY_AGE_UNVERIFIED)), ), ( "block_bots".into(), OSD::Boolean(has(RegionFlags::DENY_BOTS)), ), ( "allow_voice_chat".into(), OSD::Boolean(has(RegionFlags::ALLOW_VOICE)), ), ( "override_public_access".into(), OSD::Boolean(has(RegionFlags::ALLOW_ACCESS_OVERRIDE)), ), ("invoice".into(), OSD::UUID(UUID::random()?)), ])); let body = OSDParser::serialize_llsd_xml_bytes(payload)?; let (response, _) = self .inner .client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".into(), body, token, None, ) .await?; if (200..300).contains(&response.status_code) { return Ok(()); } } self.send_estate_change_info_dataserver(estate_name, sun_hour, flags) } pub async fn set_region_info( &self, terraform: bool, fly: bool, fly_over: bool, damage: bool, resell: bool, pushing: bool, join: bool, agents: f32, bonus: f32, block_search: bool, maturity: EstateToolsRegionMaturity, ) -> Result<(), Error> { let token = CancellationToken::default(); if let Some(uri) = self.capability("DispatchRegionInfo", &token).await? { if !crate::is_offline_fixture_uri(&uri) && !self.inner.live_mutations.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let payload = OSD::Map(HashMap::from([ ("block_terraform".into(), OSD::Boolean(terraform)), ("block_fly".into(), OSD::Boolean(fly)), ("block_fly_over".into(), OSD::Boolean(fly_over)), ("allow_damage".into(), OSD::Boolean(damage)), ("allow_land_resell".into(), OSD::Boolean(resell)), ("agent_limit".into(), OSD::Real(f64::from(agents))), ("prim_bonus".into(), OSD::Real(f64::from(bonus))), ( "sim_access".into(), OSD::String((maturity as i32).to_string()), ), ("restrict_pushobject".into(), OSD::Boolean(pushing)), ("allow_parcel_changes".into(), OSD::Boolean(join)), ("block_parcel_search".into(), OSD::Boolean(block_search)), ])); let body = OSDParser::serialize_llsd_xml_bytes(payload)?; let (response, _) = self .inner .client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".into(), body, token, None, ) .await?; if (200..300).contains(&response.status_code) { return Ok(()); } } self.set_region_info_udp( terraform, fly, damage, resell, pushing, join, agents, bonus, maturity, ) } pub async fn send_sim_console_command( &self, command: String, response_timeout: Option, ct: Option, ) -> Result, Error> { let token = ct.unwrap_or_default(); token.throw_if_cancellation_requested()?; let console_lock = self.inner.sim_console_lock.lock().fuse(); let lock_cancelled = token.cancelled().fuse(); pin_mut!(console_lock, lock_cancelled); let _console_guard = select_biased! { guard = console_lock => guard, () = lock_cancelled => return Err(Error::Cancelled), }; token.throw_if_cancellation_requested()?; let (response_sender, response_receiver) = oneshot::channel(); let response_sender = Arc::new(Mutex::new(Some(response_sender))); let sender = Arc::clone(&response_sender); let _subscription = self.inner.network.subscribe_caps( "SimConsoleResponse".into(), crate::CapsEventQueueCallback::from_handler(move |_name, message, _simulator| { let body = message.serialize().ok().and_then(|map| { map.get("body") .or_else(|| map.get("Body")) .and_then(|value| value.as_string().ok()) }); if let Some(sender) = sender .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take() { let _ = sender.send(body); } }), ); // Register the event waiter before POSTing: simulators may enqueue the // response immediately. An unconnected client is still allowed to wait // so recorded event-queue playback remains fully offline-testable. match self.capability("SimConsoleAsync", &token).await { Ok(Some(uri)) => { if !crate::is_offline_fixture_uri(&uri) && !self.inner.live_mutations.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([( "command".into(), OSD::String(command), )])))?; let (response, _) = self .inner .client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".into(), body, token.clone(), None, ) .await?; if !(200..300).contains(&response.status_code) { return Ok(None); } } Ok(None) | Err(Error::InvalidOperation) => {} Err(error) => return Err(error), } let (timeout_sender, timeout_receiver) = oneshot::channel(); let timeout = response_timeout.unwrap_or(Duration::from_secs(10)); std::thread::Builder::new() .name("sim-console-timeout".into()) .spawn(move || { std::thread::sleep(timeout); let _ = timeout_sender.send(()); }) .map_err(|_| Error::InvalidOperation)?; let response = response_receiver.fuse(); let cancelled = token.cancelled().fuse(); let timeout = timeout_receiver.fuse(); pin_mut!(response, cancelled, timeout); select_biased! { value = response => value.map_err(|_| Error::InvalidOperation), () = cancelled => Err(Error::Cancelled), _ = timeout => Ok(None), } } pub fn upload_terrain(&self, file_data: Vec, file_name: String) -> Result { if file_data.is_empty() || file_data.len() > 128 * 1024 * 1024 || file_name.is_empty() { return Err(Error::Argument); } let assets = self.inner.client.native_assets()?; let id = assets.native_stage_estate_upload(file_data)?; if let Err(error) = self.owner_message("terrain", vec!["upload filename".into(), file_name]) { assets.native_cancel_staged_estate_upload(id); return Err(error); } Ok(id) } } impl EstateToolsInner { fn handle_land_stat_caps(&self, message: crate::messages::linden::LandStatReplyMessage) { if self.disposed.load(Ordering::Acquire) || message.report_data_blocks.len() > 100_000 { return; } let tasks = message .report_data_blocks .into_iter() .map(|row| { let id = row.task_id; ( id, EstateTask { position: row.location, score: row.score, mono_score: row.mono_score, task_id: id, task_local_id: row.task_local_id, task_name: row.task_name, owner_name: row.owner_name, }, ) }) .collect(); let object_count = i32::try_from(message.total_object_count).unwrap_or(i32::MAX); if message.report_type == EstateToolsLandStatReportType::TopScripts as u32 { self.events.scripts.emit(TopScriptsReplyEventArgs { object_count, tasks, }); } else if message.report_type == EstateToolsLandStatReportType::TopColliders as u32 { self.events.colliders.emit(TopCollidersReplyEventArgs { object_count, tasks, }); } } fn handle_packet(&self, event: crate::network_manager::RawPacketReceivedEventArgs) { if self.disposed.load(Ordering::Acquire) || event.data.len() > 8 * 1024 * 1024 { return; } let mut position = 0; match event.packet_type { PacketType::EstateOwnerMessage => { if let Ok(packet) = crate::packets::EstateOwnerMessagePacket::new_from_bytes( &event.data, &mut position, ) { self.handle_owner_message(packet); } } PacketType::EstateCovenantReply => { if let Ok(packet) = crate::packets::EstateCovenantReplyPacket::new_from_bytes( &event.data, &mut position, ) { self.events.covenant.emit(EstateCovenantReplyEventArgs { covenant_id: packet.data.covenant_id, timestamp: i64::from(packet.data.covenant_timestamp), estate_name: wire_text(&packet.data.estate_name), estate_owner_id: packet.data.estate_owner_id, }); } } PacketType::LandStatReply => { if let Ok(packet) = crate::packets::LandStatReplyPacket::new_from_bytes(&event.data, &mut position) { let tasks: HashMap<_, _> = packet .report_data .into_iter() .map(|row| { let id = row.task_id; ( id, EstateTask { position: Vector3 { x: row.location_x, y: row.location_y, z: row.location_z, }, score: row.score, mono_score: 0.0, task_id: id, task_local_id: row.task_local_id, task_name: wire_text(&row.task_name), owner_name: wire_text(&row.owner_name), }, ) }) .collect(); let count = i32::try_from(packet.request_data.total_object_count).unwrap_or(i32::MAX); if packet.request_data.report_type == EstateToolsLandStatReportType::TopScripts as u32 { self.events.scripts.emit(TopScriptsReplyEventArgs { object_count: count, tasks, }); } else if packet.request_data.report_type == EstateToolsLandStatReportType::TopColliders as u32 { self.events.colliders.emit(TopCollidersReplyEventArgs { object_count: count, tasks, }); } } } _ => {} } } fn handle_owner_message(&self, packet: crate::packets::EstateOwnerMessagePacket) { let method = wire_text(&packet.method_data.method); let params: Vec<_> = packet .param_list .iter() .map(|row| row.parameter.as_slice()) .collect(); if method == "estateupdateinfo" && params.len() >= 5 { let Some(owner) = parse_uuid(params[1]) else { return; }; let Some(estate_id) = parse_u32(params[2]) else { return; }; let Some(flags) = parse_u64(params[3]) else { return; }; let Some(sun_hour) = parse_i32(params[4]) else { return; }; self.events.update.emit(EstateUpdateInfoReplyEventArgs { estate_name: wire_text(params[0]), estate_owner: owner, estate_id, flags: RegionFlags(flags), sun_hour, }); } else if method == "setaccess" && params.len() > 5 { let Some(estate_id) = parse_u32(params[0]) else { return; }; let Some(access_type) = parse_u32(params[1]) else { return; }; let ids: Vec<_> = params[6..] .iter() .filter_map(|value| parse_uuid(value)) .collect(); match access_type { 17 => self.events.users.emit(EstateUsersReplyEventArgs { estate_id, count: parse_i32(params[2]).unwrap_or_default(), allowed_users: ids, }), 18 => self.events.groups.emit(EstateGroupsReplyEventArgs { estate_id, count: parse_i32(params[3]).unwrap_or_default(), allowed_groups: ids, }), 20 => self.events.bans.emit(EstateBansReplyEventArgs { estate_id, count: parse_i32(params[4]).unwrap_or_default(), banned: ids, }), 24 => self.events.managers.emit(EstateManagersReplyEventArgs { estate_id, count: parse_i32(params[5]).unwrap_or_default(), managers: ids, }), _ => {} } } else if method == "setexperience" && params.len() >= 5 { let blocked_count = usize::try_from(parse_i32(params[2]).unwrap_or_default()).unwrap_or_default(); let trusted_count = usize::try_from(parse_i32(params[3]).unwrap_or_default()).unwrap_or_default(); let allowed_count = usize::try_from(parse_i32(params[4]).unwrap_or_default()).unwrap_or_default(); let ids: Vec<_> = params[5..] .iter() .filter_map(|value| parse_uuid(value)) .collect(); let blocked_end = blocked_count.min(ids.len()); let trusted_end = blocked_end.saturating_add(trusted_count).min(ids.len()); let allowed_end = trusted_end.saturating_add(allowed_count).min(ids.len()); self.events .experiences .emit(EstateExperienceReplyEventArgs { blocked: ids[..blocked_end].to_vec(), trusted: ids[blocked_end..trusted_end].to_vec(), allowed: ids[trusted_end..allowed_end].to_vec(), }); } } } fn yn(value: bool) -> String { if value { "Y" } else { "N" }.into() } fn wire_text(value: &[u8]) -> String { String::from_utf8_lossy(value.split(|byte| *byte == 0).next().unwrap_or_default()).into_owned() } fn parse_u32(value: &[u8]) -> Option { wire_text(value).parse().ok() } fn parse_u64(value: &[u8]) -> Option { wire_text(value).parse().ok() } fn parse_i32(value: &[u8]) -> Option { wire_text(value).parse().ok() } fn parse_uuid(value: &[u8]) -> Option { UUID::new_with_bytes_int32(value.to_vec(), 0) .ok() .or_else(|| UUID::new_with_string(wire_text(value)).ok()) } fn days_to_letters(days: RegionRestartDays) -> String { [ ('S', RegionRestartDays::SUNDAY), ('M', RegionRestartDays::MONDAY), ('T', RegionRestartDays::TUESDAY), ('W', RegionRestartDays::WEDNESDAY), ('R', RegionRestartDays::THURSDAY), ('F', RegionRestartDays::FRIDAY), ('A', RegionRestartDays::SATURDAY), ] .into_iter() .filter_map(|(letter, day)| (days.0 & day.0 != 0).then_some(letter)) .collect() } fn letters_to_days(value: &str) -> RegionRestartDays { RegionRestartDays(value.chars().fold(0, |days, letter| { days | match letter.to_ascii_uppercase() { 'S' => RegionRestartDays::SUNDAY.0, 'M' => RegionRestartDays::MONDAY.0, 'T' => RegionRestartDays::TUESDAY.0, 'W' => RegionRestartDays::WEDNESDAY.0, 'R' => RegionRestartDays::THURSDAY.0, 'F' => RegionRestartDays::FRIDAY.0, 'A' => RegionRestartDays::SATURDAY.0, _ => 0, } })) }