Files
MetaCrate/crates/libremetaverse/src/estate_tools.rs
Chili Palmer 763e9e5044
All checks were successful
Native code generation / deterministic (push) Successful in 15m57s
Imaging and meshing gate / native (push) Successful in 5m16s
Native Rust workspace compile / compile (push) Successful in 5m11s
Implement estate and marketplace services (#73)
2026-08-10 19:25:31 +00:00

1490 lines
50 KiB
Rust

//! 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<Self, Error> {
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<Self, Error> {
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<UUID>,
}
impl $name {
pub fn new(estate_id: u32, count: i32, $field: Vec<UUID>) -> Result<Self, Error> {
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<UUID> {
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<UUID>,
trusted: Vec<UUID>,
allowed: Vec<UUID>,
}
impl EstateExperienceReplyEventArgs {
pub fn new(blocked: Vec<UUID>, trusted: Vec<UUID>, allowed: Vec<UUID>) -> Result<Self, Error> {
Ok(Self {
blocked,
trusted,
allowed,
})
}
pub fn blocked(&self) -> Vec<UUID> {
self.blocked.clone()
}
pub fn trusted(&self) -> Vec<UUID> {
self.trusted.clone()
}
pub fn allowed(&self) -> Vec<UUID> {
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<Self, Error> {
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<Self, Error> {
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<UUID, EstateTask>,
}
impl $name {
pub fn new(object_count: i32, tasks: HashMap<UUID, EstateTask>) -> Result<Self, Error> {
Ok(Self {
object_count,
tasks,
})
}
pub const fn object_count(&self) -> i32 {
self.object_count
}
pub fn tasks(&self) -> HashMap<UUID, EstateTask> {
self.tasks.clone()
}
}
};
}
task_args!(TopCollidersReplyEventArgs);
task_args!(TopScriptsReplyEventArgs);
#[derive(Default)]
struct EstateEvents {
bans: EventRegistry<EstateBansReplyEventArgs>,
covenant: EventRegistry<EstateCovenantReplyEventArgs>,
experiences: EventRegistry<EstateExperienceReplyEventArgs>,
groups: EventRegistry<EstateGroupsReplyEventArgs>,
managers: EventRegistry<EstateManagersReplyEventArgs>,
update: EventRegistry<EstateUpdateInfoReplyEventArgs>,
users: EventRegistry<EstateUsersReplyEventArgs>,
colliders: EventRegistry<TopCollidersReplyEventArgs>,
scripts: EventRegistry<TopScriptsReplyEventArgs>,
}
pub(crate) struct EstateToolsInner {
client: GridClient,
network: crate::NetworkManager,
events: EstateEvents,
disposed: AtomicBool,
live_mutations: AtomicBool,
sim_console_lock: tokio::sync::Mutex<()>,
subscriptions: Mutex<Vec<Subscription>>,
}
pub struct EstateTools {
pub ground_texture_limits: EstateToolsGroundTextureHeightSettings,
pub ground_textures: EstateToolsGroundTextureSettings,
pub(crate) inner: Arc<EstateToolsInner>,
}
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<Self, Error> {
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<EstateToolsInner>) -> Self {
Self {
ground_texture_limits: blank_limits(),
ground_textures: blank_textures(),
inner,
}
}
pub(crate) fn native_inner(&self) -> Arc<EstateToolsInner> {
Arc::clone(&self.inner)
}
pub fn new(client: GridClient) -> Result<Self, Error> {
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<EstateBansReplyEventArgs>,
) -> Subscription {
self.inner.events.bans.subscribe(h)
}
pub fn subscribe_estate_covenant_reply(
&self,
h: EventHandler<EstateCovenantReplyEventArgs>,
) -> Subscription {
self.inner.events.covenant.subscribe(h)
}
pub fn subscribe_estate_experience_reply(
&self,
h: EventHandler<EstateExperienceReplyEventArgs>,
) -> Subscription {
self.inner.events.experiences.subscribe(h)
}
pub fn subscribe_estate_groups_reply(
&self,
h: EventHandler<EstateGroupsReplyEventArgs>,
) -> Subscription {
self.inner.events.groups.subscribe(h)
}
pub fn subscribe_estate_managers_reply(
&self,
h: EventHandler<EstateManagersReplyEventArgs>,
) -> Subscription {
self.inner.events.managers.subscribe(h)
}
pub fn subscribe_estate_update_info_reply(
&self,
h: EventHandler<EstateUpdateInfoReplyEventArgs>,
) -> Subscription {
self.inner.events.update.subscribe(h)
}
pub fn subscribe_estate_users_reply(
&self,
h: EventHandler<EstateUsersReplyEventArgs>,
) -> Subscription {
self.inner.events.users.subscribe(h)
}
pub fn subscribe_top_colliders_reply(
&self,
h: EventHandler<TopCollidersReplyEventArgs>,
) -> Subscription {
self.inner.events.colliders.subscribe(h)
}
pub fn subscribe_top_scripts_reply(
&self,
h: EventHandler<TopScriptsReplyEventArgs>,
) -> 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<crate::Simulator, Error> {
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<T: GeneratedPacket>(&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<String>,
) -> 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<String>) -> 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<Option<Uri>, 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<CancellationToken>,
) -> Result<Option<RegionRestartSchedule>, 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<CancellationToken>,
) -> Result<bool, Error> {
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<CancellationToken>,
) -> Result<Option<crate::assets::Asset>, 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<CancellationToken>,
) -> Result<Option<crate::assets::Asset>, 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<CancellationToken>,
) -> 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<Duration>,
ct: Option<CancellationToken>,
) -> Result<Option<String>, 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<u8>, file_name: String) -> Result<UUID, Error> {
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<u32> {
wire_text(value).parse().ok()
}
fn parse_u64(value: &[u8]) -> Option<u64> {
wire_text(value).parse().ok()
}
fn parse_i32(value: &[u8]) -> Option<i32> {
wire_text(value).parse().ok()
}
fn parse_uuid(value: &[u8]) -> Option<UUID> {
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,
}
}))
}