1490 lines
50 KiB
Rust
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,
|
|
}
|
|
}))
|
|
}
|