Files
MetaCrate/crates/libremetaverse/src/interest_list.rs
Chili Palmer 66fbb87b3f
All checks were successful
Native code generation / deterministic (push) Successful in 15m16s
Imaging and meshing gate / native (push) Successful in 5m9s
JPEG 2000 feature / linux (push) Successful in 2m47s
Native Rust workspace compile / compile (push) Successful in 5m14s
Skia feature / linux (push) Successful in 31m28s
Implement grid and directory discovery (#71)
2026-08-10 17:45:08 +00:00

238 lines
8.1 KiB
Rust

//! Capability-backed interest-list mode control.
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::unnecessary_wraps)] // Fixed mapped constructor is fallible.
use crate::{Error, GridClient, Simulator};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::CancellationToken;
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct InterestListMessage {
mode: String,
interest_list_mode: crate::messages::linden::InterestListMode,
}
impl InterestListMessage {
pub fn new() -> Result<Self, Error> {
Ok(Self {
mode: "default".into(),
interest_list_mode: crate::messages::linden::InterestListMode::Default,
})
}
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
let mode = map
.get("mode")
.map(|value| value.as_string())
.transpose()?
.unwrap_or_else(|| "default".into());
self.set_mode(mode);
Ok(())
}
pub fn serialize(&self) -> Result<OSDMap, Error> {
OSDMap::new_with_dictionary(std::collections::HashMap::from([(
"mode".into(),
OSD::String(self.mode.clone()),
)]))
}
pub fn interest_list_mode(&self) -> crate::messages::linden::InterestListMode {
self.interest_list_mode
}
pub fn set_interest_list_mode(&mut self, value: crate::messages::linden::InterestListMode) {
self.interest_list_mode = value;
self.mode = mode_name(value).into();
}
pub fn mode(&self) -> String {
self.mode.clone()
}
pub fn set_mode(&mut self, value: String) {
self.interest_list_mode = if matches!(
value.to_ascii_lowercase().as_str(),
"360" | "panoramic360" | "panoramic_360"
) {
crate::messages::linden::InterestListMode::Panoramic360
} else {
crate::messages::linden::InterestListMode::Default
};
self.mode = mode_name(self.interest_list_mode).into();
}
}
impl crate::interfaces::IMessage for InterestListMessage {
fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
InterestListMessage::deserialize(self, map)
}
fn serialize(&self) -> Result<OSDMap, Error> {
InterestListMessage::serialize(self)
}
}
fn mode_name(mode: crate::messages::linden::InterestListMode) -> &'static str {
match mode {
crate::messages::linden::InterestListMode::Default => "default",
crate::messages::linden::InterestListMode::Panoramic360 => "360",
}
}
pub(crate) struct InterestListManagerInner {
client: Arc<GridClient>,
current_mode: RwLock<crate::messages::linden::InterestListMode>,
}
#[derive(Clone)]
pub struct InterestListManager {
pub(crate) inner: Arc<InterestListManagerInner>,
}
impl InterestListManager {
pub fn new(client: GridClient) -> Result<Self, Error> {
Self::native_new(Arc::new(client))
}
pub(crate) fn native_new(client: Arc<GridClient>) -> Result<Self, Error> {
if let Some(inner) = client.cached_interest_list_manager_inner() {
return Ok(Self { inner });
}
let inner = Arc::new(InterestListManagerInner {
client: Arc::clone(&client),
current_mode: RwLock::new(crate::messages::linden::InterestListMode::Default),
});
Ok(Self {
inner: client.install_interest_list_manager_inner(inner),
})
}
pub fn dispose(&self) -> Result<(), Error> {
Ok(())
}
pub fn current_mode(&self) -> crate::messages::linden::InterestListMode {
*self
.inner
.current_mode
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub async fn reset(&self, cancellation_token: Option<CancellationToken>) -> Result<(), Error> {
self.set_mode(
crate::messages::linden::InterestListMode::Default,
cancellation_token,
)
.await
}
pub async fn set_mode(
&self,
mode: crate::messages::linden::InterestListMode,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token());
token.throw_if_cancellation_requested()?;
let simulations = self.inner.client.network().simulators.snapshot();
let mut accepted = false;
for simulator in simulations {
if self
.set_mode_on_sim(simulator, mode, Some(token.clone()))
.await?
{
accepted = true;
}
}
if accepted || self.inner.client.network().simulators.is_empty() {
*self
.inner
.current_mode
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = mode;
}
Ok(())
}
pub async fn reset_on_sim(
&self,
simulator: Simulator,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.set_mode_on_sim(
simulator,
crate::messages::linden::InterestListMode::Default,
cancellation_token,
)
.await
}
pub async fn set_mode_on_sim(
&self,
simulator: Simulator,
mode: crate::messages::linden::InterestListMode,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token());
token.throw_if_cancellation_requested()?;
let Some(caps) = simulator.native_caps() else {
return Ok(false);
};
let Some(uri) = caps.capability_uri("InterestList".into())? else {
return Ok(false);
};
let mut message = InterestListMessage::new()?;
message.set_interest_list_mode(mode);
let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(message.serialize()?.snapshot()))?;
let (response, bytes) = 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?;
token.throw_if_cancellation_requested()?;
if bytes.len() > 1024 * 1024 {
return Err(Error::Argument);
}
let ok = (200..300).contains(&response.status_code);
if ok {
*self
.inner
.current_mode
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = mode;
}
Ok(ok)
}
}
#[cfg(test)]
mod tests {
use super::*;
use libremetaverse_types::compat::CancellationTokenSource;
#[test]
fn message_round_trip() {
let mut value = InterestListMessage::new().unwrap();
value.set_interest_list_mode(crate::messages::linden::InterestListMode::Panoramic360);
let mut copy = InterestListMessage::new().unwrap();
copy.deserialize(value.serialize().unwrap()).unwrap();
assert_eq!(copy.mode(), "360");
assert_eq!(
copy.interest_list_mode(),
crate::messages::linden::InterestListMode::Panoramic360
);
}
#[tokio::test]
async fn cancelled_mode_change_does_not_commit() {
let client = GridClient::new().unwrap();
let manager = InterestListManager::new(client).unwrap();
let source = CancellationTokenSource::new();
source.cancel();
assert_eq!(
manager
.set_mode(
crate::messages::linden::InterestListMode::Panoramic360,
Some(source.token())
)
.await,
Err(Error::Cancelled)
);
assert_eq!(
manager.current_mode(),
crate::messages::linden::InterestListMode::Default
);
}
}