Implement capability event queue processing (#55)
This commit is contained in:
@@ -294,3 +294,9 @@ retry handling while preserving permanent 401/403/404/410 failures. Capability
|
||||
URLs and tokens are absent from errors and diagnostics. Ownership, limits,
|
||||
injection, and offline fake-server coverage are documented in
|
||||
[`docs/caps-http.md`](docs/caps-http.md).
|
||||
Seed-cap discovery and `EventQueueGet` are native Rust too: the full reference
|
||||
capability list is posted as LLSD/XML, accepted URIs are rate-categorized, and
|
||||
bounded long polls preserve ack IDs, reconnect retries, shutdown `done`, and
|
||||
event order. Typed Linden messages share the CAPS dispatch registry; unknown
|
||||
messages remain observable and then use the generated caps-to-packet catalog so
|
||||
packet handlers receive them through the same bounded pipeline as UDP traffic.
|
||||
|
||||
@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
||||
|
||||
| Assembly | Types | Members | Status |
|
||||
|---|---:|---:|---|
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 53 types / 13,673 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 68 types / 13,858 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
|
||||
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
|
||||
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |
|
||||
|
||||
773
crates/libremetaverse/src/caps.rs
Normal file
773
crates/libremetaverse/src/caps.rs
Normal file
@@ -0,0 +1,773 @@
|
||||
//! Seed-capability discovery and capability event routing.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Public Result shapes are fixed by the compatibility map.
|
||||
#![allow(clippy::needless_pass_by_value)] // Mapped APIs preserve owned CLR argument shapes.
|
||||
|
||||
use crate::event_queue::{
|
||||
EventQueueClient, EventQueueClientConnectedCallback, EventQueueClientEventCallback,
|
||||
};
|
||||
use crate::packets::Packet;
|
||||
use crate::{Error, Simulator};
|
||||
use libremetaverse_structured_data::{OSD, OSDArray, OSDFormat, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, CancellationTokenSource, EventHandler, Subscription, Uri,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock, Weak};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
const INITIAL_SEED_RETRY: Duration = Duration::from_secs(1);
|
||||
const MAXIMUM_SEED_RETRY: Duration = Duration::from_secs(30);
|
||||
|
||||
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
|
||||
value
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
|
||||
value
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
struct CapabilitiesEventState {
|
||||
next_id: AtomicU64,
|
||||
handlers: Mutex<Vec<(u64, EventHandler<CapabilitiesReceivedEventArgs>)>>,
|
||||
}
|
||||
|
||||
impl Default for CapabilitiesEventState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
handlers: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CapabilitiesReceivedEventArgs {
|
||||
simulator: Simulator,
|
||||
}
|
||||
|
||||
impl CapabilitiesReceivedEventArgs {
|
||||
pub const fn new(simulator: Simulator) -> Result<Self, Error> {
|
||||
Ok(Self { simulator })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn simulator(&self) -> Simulator {
|
||||
self.simulator.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct SeedTask {
|
||||
cancellation: CancellationTokenSource,
|
||||
handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub(crate) struct CapsInner {
|
||||
simulator: Weak<crate::network_manager::SimulatorData>,
|
||||
seed_uri: Uri,
|
||||
caps: RwLock<HashMap<String, Uri>>,
|
||||
event_queue: Mutex<Option<EventQueueClient>>,
|
||||
seed_task: Mutex<Option<SeedTask>>,
|
||||
events: Arc<CapabilitiesEventState>,
|
||||
disconnected: AtomicBool,
|
||||
}
|
||||
|
||||
impl CapsInner {
|
||||
fn simulator(&self) -> Option<Simulator> {
|
||||
Simulator::native_from_weak(&self.simulator)
|
||||
}
|
||||
|
||||
pub(crate) fn disconnect(&self, immediate: bool) {
|
||||
if self.disconnected.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
if let Some(task) = mutex(&self.seed_task).take() {
|
||||
task.cancellation.cancel();
|
||||
if task.handle.thread().id() != thread::current().id() {
|
||||
let _ = task.handle.join();
|
||||
}
|
||||
}
|
||||
if let Some(queue) = mutex(&self.event_queue).take() {
|
||||
let _ = queue.stop(immediate);
|
||||
let _ = queue.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_capabilities_received(&self, simulator: Simulator) {
|
||||
let handlers: Vec<_> = mutex(&self.events.handlers)
|
||||
.iter()
|
||||
.map(|(_, handler)| Arc::clone(handler))
|
||||
.collect();
|
||||
for handler in handlers {
|
||||
let args = CapabilitiesReceivedEventArgs {
|
||||
simulator: simulator.clone(),
|
||||
};
|
||||
let _ = catch_unwind(AssertUnwindSafe(|| handler(args)));
|
||||
}
|
||||
}
|
||||
|
||||
fn route_event(&self, event_name: String, body: OSDMap) {
|
||||
let Some(simulator) = self.simulator() else {
|
||||
return;
|
||||
};
|
||||
match crate::message_decoder::decode_event(&event_name, &body) {
|
||||
Ok(Some(message)) => {
|
||||
simulator.native_dispatch_caps_event(&event_name, message.as_ref());
|
||||
}
|
||||
Ok(None) | Err(_) => {
|
||||
if let Ok(Some(packet)) = Packet::build_packet_with_string_osd_map(event_name, body)
|
||||
{
|
||||
let _ = simulator.native_enqueue_caps_packet(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CapsInner {
|
||||
fn drop(&mut self) {
|
||||
self.disconnected.store(true, Ordering::Release);
|
||||
if let Some(task) = self
|
||||
.seed_task
|
||||
.get_mut()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take()
|
||||
{
|
||||
task.cancellation.cancel();
|
||||
if task.handle.thread().id() != thread::current().id() {
|
||||
let _ = task.handle.join();
|
||||
}
|
||||
}
|
||||
if let Some(queue) = self
|
||||
.event_queue
|
||||
.get_mut()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take()
|
||||
{
|
||||
let _ = queue.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Capability collection associated with a simulator.
|
||||
pub struct Caps {
|
||||
pub simulator: Box<Simulator>,
|
||||
inner: Arc<CapsInner>,
|
||||
}
|
||||
|
||||
impl Clone for Caps {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
simulator: Box::new(self.simulator.native_clone_without_caps()),
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Caps {
|
||||
pub(crate) fn native_create(
|
||||
simulator: &Simulator,
|
||||
seed_uri: Uri,
|
||||
) -> Result<Arc<CapsInner>, Error> {
|
||||
if !is_http_uri(&seed_uri) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let inner = Arc::new(CapsInner {
|
||||
simulator: simulator.native_data_weak(),
|
||||
seed_uri,
|
||||
caps: RwLock::new(HashMap::new()),
|
||||
event_queue: Mutex::new(None),
|
||||
seed_task: Mutex::new(None),
|
||||
events: Arc::new(CapabilitiesEventState::default()),
|
||||
disconnected: AtomicBool::new(false),
|
||||
});
|
||||
start_seed_task(&inner)?;
|
||||
Ok(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_from_inner(inner: Arc<CapsInner>, simulator: Simulator) -> Self {
|
||||
Self {
|
||||
simulator: Box::new(simulator.native_clone_without_caps()),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_capabilities_received(
|
||||
&self,
|
||||
handler: Option<EventHandler<CapabilitiesReceivedEventArgs>>,
|
||||
) -> Subscription {
|
||||
let Some(handler) = handler else {
|
||||
return Subscription::detached();
|
||||
};
|
||||
let id = self.inner.events.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
mutex(&self.inner.events.handlers).push((id, handler));
|
||||
let state = Arc::downgrade(&self.inner.events);
|
||||
Subscription::new(move || {
|
||||
if let Some(state) = state.upgrade() {
|
||||
mutex(&state.handlers).retain(|(candidate, _)| *candidate != id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns the fixed capability names requested from a seed capability.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics only if constructing an in-memory LLSD array fails.
|
||||
pub fn all_capabilities() -> OSDArray {
|
||||
OSDArray::new_with_list(
|
||||
ALL_CAPABILITIES
|
||||
.iter()
|
||||
.map(|name| OSD::String((*name).to_owned()))
|
||||
.collect(),
|
||||
)
|
||||
.expect("static capability list is valid")
|
||||
}
|
||||
|
||||
pub fn capabilities(&self) -> Result<Vec<String>, Error> {
|
||||
let mut names: Vec<_> = read(&self.inner.caps).keys().cloned().collect();
|
||||
names.sort_unstable();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub fn capability_name_from_uri(&self, cap: Uri) -> Result<String, Error> {
|
||||
read(&self.inner.caps)
|
||||
.iter()
|
||||
.find_map(|(name, uri)| (uri == &cap).then(|| name.clone()))
|
||||
.ok_or(Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn capability_uri(&self, capability: String) -> Result<Option<Uri>, Error> {
|
||||
Ok(read(&self.inner.caps).get(&capability).cloned())
|
||||
}
|
||||
|
||||
pub fn disconnect(&self, immediate: bool) -> Result<(), Error> {
|
||||
self.inner.disconnect(immediate);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_mesh_cap_uri(&self) -> Result<Option<Uri>, Error> {
|
||||
let caps = read(&self.inner.caps);
|
||||
Ok(caps
|
||||
.get("ViewerAsset")
|
||||
.or_else(|| caps.get("GetMesh2"))
|
||||
.or_else(|| caps.get("GetMesh"))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
pub fn get_texture_cap_uri(&self) -> Result<Option<Uri>, Error> {
|
||||
let caps = read(&self.inner.caps);
|
||||
Ok(caps
|
||||
.get("ViewerAsset")
|
||||
.or_else(|| caps.get("GetTexture"))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn event_queue(&self) -> Option<EventQueueClient> {
|
||||
mutex(&self.inner.event_queue).clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_event_queue_running(&self) -> bool {
|
||||
mutex(&self.inner.event_queue)
|
||||
.as_ref()
|
||||
.is_some_and(EventQueueClient::running)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn seed_caps_uri(&self) -> Uri {
|
||||
self.inner.seed_uri.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Caps {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("Caps")
|
||||
.field("capability_count", &read(&self.inner.caps).len())
|
||||
.field("event_queue_running", &self.is_event_queue_running())
|
||||
.field(
|
||||
"disconnected",
|
||||
&self.inner.disconnected.load(Ordering::Acquire),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
fn start_seed_task(inner: &Arc<CapsInner>) -> Result<(), Error> {
|
||||
let cancellation = CancellationTokenSource::new();
|
||||
let token = cancellation.token();
|
||||
let weak = Arc::downgrade(inner);
|
||||
let (ready_sender, ready_receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let handle = thread::Builder::new()
|
||||
.name("libremetaverse-seed-caps".to_owned())
|
||||
.spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build();
|
||||
let Ok(runtime) = runtime else {
|
||||
let _ = ready_sender.send(false);
|
||||
return;
|
||||
};
|
||||
let _ = ready_sender.send(true);
|
||||
runtime.block_on(run_seed_requests(weak, token));
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if ready_receiver.recv_timeout(Duration::from_secs(2)) != Ok(true) {
|
||||
cancellation.cancel();
|
||||
let _ = handle.join();
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
*mutex(&inner.seed_task) = Some(SeedTask {
|
||||
cancellation,
|
||||
handle,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_seed_requests(weak: Weak<CapsInner>, cancellation: CancellationToken) {
|
||||
let mut retry = 0_u32;
|
||||
loop {
|
||||
let Some(inner) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if cancellation.is_cancellation_requested() || inner.disconnected.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
let Some(simulator) = inner.simulator() else {
|
||||
return;
|
||||
};
|
||||
if !simulator.native_is_connected() {
|
||||
return;
|
||||
}
|
||||
let http = simulator.client.native_http_caps_client();
|
||||
let response = http
|
||||
.post_with_uri_osd_format_osd_cancellation_token_i_progress(
|
||||
inner.seed_uri.clone(),
|
||||
OSDFormat::Xml,
|
||||
OSD::Array(Caps::all_capabilities().snapshot()),
|
||||
cancellation.clone(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
match response {
|
||||
Ok((response, data)) if response.is_success_status_code() => {
|
||||
if install_seed_response(&inner, &simulator, data).is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok((response, _)) if response.status_code == 404 => return,
|
||||
Err(Error::Cancelled) if cancellation.is_cancellation_requested() => return,
|
||||
Ok(_) | Err(_) => {}
|
||||
}
|
||||
retry = retry.saturating_add(1);
|
||||
let delay = seed_retry_delay(&inner.seed_uri, retry);
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(delay) => {}
|
||||
() = cancellation.cancelled() => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install_seed_response(
|
||||
inner: &Arc<CapsInner>,
|
||||
simulator: &Simulator,
|
||||
data: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let OSD::Map(map) = OSDParser::deserialize_with_bytes(data)? else {
|
||||
return Err(Error::Parse {
|
||||
position: 0,
|
||||
context: "seed capability response is not a map",
|
||||
});
|
||||
};
|
||||
let mut discovered = HashMap::with_capacity(map.len());
|
||||
for (name, value) in map {
|
||||
if let Some(uri) = value.as_uri()? {
|
||||
if !is_http_uri(&uri) {
|
||||
continue;
|
||||
}
|
||||
simulator
|
||||
.client
|
||||
.native_caps_rate_limiter()
|
||||
.register_cap_uri(name.clone(), uri.clone())?;
|
||||
discovered.insert(name, uri);
|
||||
}
|
||||
}
|
||||
*write(&inner.caps) = discovered;
|
||||
|
||||
if let Some(uri) = read(&inner.caps).get("EventQueueGet").cloned() {
|
||||
let mut queue = EventQueueClient::new_for_caps(uri, simulator.native_clone_without_caps())?;
|
||||
let connected_inner = Arc::downgrade(inner);
|
||||
queue.on_connected = Some(EventQueueClientConnectedCallback::from_handler(move || {
|
||||
if let Some(inner) = connected_inner.upgrade()
|
||||
&& let Some(simulator) = inner.simulator()
|
||||
{
|
||||
simulator.native_raise_event_queue_running();
|
||||
}
|
||||
}));
|
||||
let event_inner = Arc::downgrade(inner);
|
||||
queue.on_event = Some(EventQueueClientEventCallback::from_handler(
|
||||
move |event_name, body| {
|
||||
if let Some(inner) = event_inner.upgrade() {
|
||||
inner.route_event(event_name, body);
|
||||
}
|
||||
},
|
||||
));
|
||||
queue.start()?;
|
||||
*mutex(&inner.event_queue) = Some(queue);
|
||||
}
|
||||
inner.emit_capabilities_received(simulator.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seed_retry_delay(uri: &Uri, retry: u32) -> Duration {
|
||||
let multiplier = 1_u32
|
||||
.checked_shl(retry.saturating_sub(1).min(30))
|
||||
.unwrap_or(u32::MAX);
|
||||
let base = INITIAL_SEED_RETRY
|
||||
.saturating_mul(multiplier)
|
||||
.min(MAXIMUM_SEED_RETRY);
|
||||
let mut hash = 0xcbf2_9ce4_8422_2325_u64 ^ u64::from(retry);
|
||||
for byte in uri.0.as_bytes() {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100_0000_01b3);
|
||||
}
|
||||
let max_jitter = base / 8;
|
||||
if max_jitter.is_zero() {
|
||||
return base;
|
||||
}
|
||||
let nanos = u64::try_from(max_jitter.as_nanos())
|
||||
.unwrap_or(u64::MAX)
|
||||
.saturating_add(1);
|
||||
base.saturating_add(Duration::from_nanos(hash % nanos))
|
||||
}
|
||||
|
||||
fn is_http_uri(uri: &Uri) -> bool {
|
||||
reqwest::Url::parse(&uri.0)
|
||||
.ok()
|
||||
.is_some_and(|uri| matches!(uri.scheme(), "http" | "https") && uri.host().is_some())
|
||||
}
|
||||
|
||||
const ALL_CAPABILITIES: &[&str] = &[
|
||||
"AbuseCategories",
|
||||
"AcceptFriendship",
|
||||
"AcceptGroupInvite",
|
||||
"AgentPreferences",
|
||||
"AgentProfile",
|
||||
"AgentState",
|
||||
"AttachmentResources",
|
||||
"AvatarPickerSearch",
|
||||
"AvatarRenderInfo",
|
||||
"CharacterProperties",
|
||||
"ChatSessionRequest",
|
||||
"CopyInventoryFromNotecard",
|
||||
"CreateInventoryCategory",
|
||||
"DeclineFriendship",
|
||||
"DeclineGroupInvite",
|
||||
"DispatchRegionInfo",
|
||||
"DirectDelivery",
|
||||
"EnvironmentSettings",
|
||||
"EstateAccess",
|
||||
"EstateChangeInfo",
|
||||
"EventQueueGet",
|
||||
"ExtEnvironment",
|
||||
"FetchLib2",
|
||||
"FetchLibDescendents2",
|
||||
"FetchInventory2",
|
||||
"FetchInventoryDescendents2",
|
||||
"IncrementCOFVersion",
|
||||
"RequestTaskInventory",
|
||||
"InterestList",
|
||||
"InventoryThumbnailUpload",
|
||||
"GetDisplayNames",
|
||||
"GetExperiences",
|
||||
"AgentExperiences",
|
||||
"FindExperienceByName",
|
||||
"GetExperienceInfo",
|
||||
"GetAdminExperiences",
|
||||
"GetCreatorExperiences",
|
||||
"ExperiencePreferences",
|
||||
"GroupExperiences",
|
||||
"UpdateExperience",
|
||||
"IsExperienceAdmin",
|
||||
"IsExperienceContributor",
|
||||
"RegionExperiences",
|
||||
"ExperienceQuery",
|
||||
"GetMesh",
|
||||
"GetMesh2",
|
||||
"GetMetadata",
|
||||
"GetObjectCost",
|
||||
"GetObjectPhysicsData",
|
||||
"GetTexture",
|
||||
"GroupAPIv1",
|
||||
"GroupMemberData",
|
||||
"GroupProposalBallot",
|
||||
"HomeLocation",
|
||||
"LandResources",
|
||||
"LSLSyntax",
|
||||
"MapLayer",
|
||||
"MapLayerGod",
|
||||
"MeshUploadFlag",
|
||||
"ModifyMaterialParams",
|
||||
"ModifyRegion",
|
||||
"NavMeshGenerationStatus",
|
||||
"NewFileAgentInventory",
|
||||
"NewFileAgentInventoryVariablePrice",
|
||||
"ObjectAnimation",
|
||||
"ObjectMedia",
|
||||
"ObjectMediaNavigate",
|
||||
"ObjectNavMeshProperties",
|
||||
"ParcelPropertiesUpdate",
|
||||
"ParcelVoiceInfoRequest",
|
||||
"ProductInfoRequest",
|
||||
"ProvisionVoiceAccountRequest",
|
||||
"ReadOfflineMsgs",
|
||||
"RegionObjects",
|
||||
"RegionSchedule",
|
||||
"RemoteParcelRequest",
|
||||
"RenderMaterials",
|
||||
"RequestTextureDownload",
|
||||
"ResourceCostSelected",
|
||||
"RetrieveNavMeshSrc",
|
||||
"SearchStatRequest",
|
||||
"SearchStatTracking",
|
||||
"SendPostcard",
|
||||
"SendUserReport",
|
||||
"SendUserReportWithScreenshot",
|
||||
"ServerReleaseNotes",
|
||||
"SetDisplayName",
|
||||
"SimConsoleAsync",
|
||||
"SimulatorFeatures",
|
||||
"StartGroupProposal",
|
||||
"TerrainNavMeshProperties",
|
||||
"TextureStats",
|
||||
"UntrustedSimulatorMessage",
|
||||
"UpdateAgentInformation",
|
||||
"UpdateAgentLanguage",
|
||||
"UpdateAvatarAppearance",
|
||||
"UpdateGestureAgentInventory",
|
||||
"UpdateGestureTaskInventory",
|
||||
"UpdateNotecardAgentInventory",
|
||||
"UpdateNotecardTaskInventory",
|
||||
"UpdateScriptAgent",
|
||||
"UpdateScriptTask",
|
||||
"UpdateSettingsAgentInventory",
|
||||
"UpdateSettingsTaskInventory",
|
||||
"UploadAgentProfileImage",
|
||||
"UpdateMaterialAgentInventory",
|
||||
"UpdateMaterialTaskInventory",
|
||||
"UploadBakedTexture",
|
||||
"UserInfo",
|
||||
"ViewerAsset",
|
||||
"ViewerBenefits",
|
||||
"ViewerMetrics",
|
||||
"ViewerStartAuction",
|
||||
"ViewerStats",
|
||||
"VoiceSignalingRequest",
|
||||
"InventoryAPIv3",
|
||||
"LibraryAPIv3",
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{GridClient, HttpCapsClient};
|
||||
use libremetaverse_types::compat::{HttpMessageHandler, HttpRequest, HttpResponse};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Instant;
|
||||
|
||||
fn lock<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn response(status_code: u16, body: Vec<u8>) -> HttpResponse {
|
||||
HttpResponse {
|
||||
status_code,
|
||||
headers: BTreeMap::new(),
|
||||
content_type: Some(HttpCapsClient::LLSD_XML.to_owned()),
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
fn seed_body() -> Vec<u8> {
|
||||
OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([
|
||||
(
|
||||
"EventQueueGet".to_owned(),
|
||||
OSD::Uri(Uri(
|
||||
"https://caps.example.test/queue?token=eq-secret".to_owned()
|
||||
)),
|
||||
),
|
||||
(
|
||||
"ViewerAsset".to_owned(),
|
||||
OSD::Uri(Uri("https://asset.example.test/viewer".to_owned())),
|
||||
),
|
||||
(
|
||||
"GetMesh2".to_owned(),
|
||||
OSD::Uri(Uri("https://asset.example.test/mesh2".to_owned())),
|
||||
),
|
||||
(
|
||||
"GetTexture".to_owned(),
|
||||
OSD::Uri(Uri("https://asset.example.test/texture".to_owned())),
|
||||
),
|
||||
(
|
||||
"InvalidScheme".to_owned(),
|
||||
OSD::Uri(Uri("file:///private/capability".to_owned())),
|
||||
),
|
||||
])))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_discovery_preserves_request_shape_registers_caps_and_starts_queue() {
|
||||
let requests = Arc::new(Mutex::new(Vec::<HttpRequest>::new()));
|
||||
let recorded = Arc::clone(&requests);
|
||||
let release_seed = Arc::new(AtomicBool::new(false));
|
||||
let release = Arc::clone(&release_seed);
|
||||
let handler = HttpMessageHandler::new(move |request, _cancellation| {
|
||||
let recorded = Arc::clone(&recorded);
|
||||
let release = Arc::clone(&release);
|
||||
async move {
|
||||
let is_seed = request.uri.0.contains("/seed");
|
||||
lock(&recorded).push(request);
|
||||
if is_seed {
|
||||
while !release.load(Ordering::Acquire) {
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
response(200, seed_body())
|
||||
} else {
|
||||
response(404, Vec::new())
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let simulator =
|
||||
Simulator::new(client, "127.0.0.1:13001".parse().unwrap(), 2, None, None).unwrap();
|
||||
simulator.native_set_connected_for_tests(true);
|
||||
simulator
|
||||
.native_set_seed_caps(
|
||||
Some(Uri(
|
||||
"https://caps.example.test/seed?token=seed-secret".to_owned()
|
||||
)),
|
||||
Some(false),
|
||||
)
|
||||
.unwrap();
|
||||
let caps = simulator.clone().caps.expect("caps snapshot");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |args| {
|
||||
sender.send(args.simulator().handle).unwrap();
|
||||
})));
|
||||
release_seed.store(true, Ordering::Release);
|
||||
assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), 2);
|
||||
|
||||
assert_eq!(
|
||||
caps.seed_caps_uri(),
|
||||
Uri("https://caps.example.test/seed?token=seed-secret".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
caps.get_texture_cap_uri().unwrap(),
|
||||
Some(Uri("https://asset.example.test/viewer".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
caps.get_mesh_cap_uri().unwrap(),
|
||||
Some(Uri("https://asset.example.test/viewer".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
caps.capability_name_from_uri(Uri("https://asset.example.test/mesh2".to_owned()))
|
||||
.unwrap(),
|
||||
"GetMesh2"
|
||||
);
|
||||
assert_eq!(
|
||||
caps.capability_uri("InvalidScheme".to_owned()).unwrap(),
|
||||
None
|
||||
);
|
||||
assert!(caps.event_queue().is_some());
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while lock(&requests).len() < 2 && Instant::now() < deadline {
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
let requests = lock(&requests);
|
||||
let OSD::Array(requested) =
|
||||
OSDParser::deserialize_llsd_xml_with_bytes(requests[0].body.clone()).unwrap()
|
||||
else {
|
||||
panic!("capability request array")
|
||||
};
|
||||
let requested: Vec<_> = requested
|
||||
.iter()
|
||||
.map(|value| value.as_string().unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
requested,
|
||||
ALL_CAPABILITIES
|
||||
.iter()
|
||||
.map(|value| (*value).to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
requests[0].content_type.as_deref(),
|
||||
Some(HttpCapsClient::LLSD_XML)
|
||||
);
|
||||
drop(requests);
|
||||
caps.disconnect(true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_capabilities_matches_reference_and_debug_redacts_seed_uri() {
|
||||
let values: Vec<_> = Caps::all_capabilities()
|
||||
.snapshot()
|
||||
.into_iter()
|
||||
.map(|value| value.as_string().unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
values,
|
||||
ALL_CAPABILITIES
|
||||
.iter()
|
||||
.map(|value| (*value).to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert!(values.contains(&"EventQueueGet".to_owned()));
|
||||
assert!(values.contains(&"InventoryAPIv3".to_owned()));
|
||||
|
||||
let handler = HttpMessageHandler::new(|_request, cancellation| async move {
|
||||
cancellation.cancelled().await;
|
||||
response(499, Vec::new())
|
||||
});
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let simulator =
|
||||
Simulator::new(client, "127.0.0.1:13002".parse().unwrap(), 3, None, None).unwrap();
|
||||
simulator.native_set_connected_for_tests(true);
|
||||
simulator
|
||||
.native_set_seed_caps(
|
||||
Some(Uri(
|
||||
"https://caps.example.test/seed?token=do-not-log".to_owned()
|
||||
)),
|
||||
Some(false),
|
||||
)
|
||||
.unwrap();
|
||||
let caps = simulator.clone().caps.unwrap();
|
||||
let debug = format!("{caps:?}");
|
||||
assert!(!debug.contains("do-not-log"));
|
||||
caps.disconnect(true).unwrap();
|
||||
}
|
||||
}
|
||||
1120
crates/libremetaverse/src/event_queue.rs
Normal file
1120
crates/libremetaverse/src/event_queue.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,11 @@ extern crate self as libremetaverse;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod attention_catalog;
|
||||
mod bit_pack;
|
||||
mod caps;
|
||||
mod caps_http;
|
||||
mod client_core;
|
||||
mod download_manager;
|
||||
mod event_queue;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod foliage_catalog;
|
||||
mod generated;
|
||||
@@ -15,6 +17,7 @@ mod generated;
|
||||
mod genepool_catalog;
|
||||
mod j2k;
|
||||
mod message_codec;
|
||||
mod message_decoder;
|
||||
mod network_manager;
|
||||
#[rustfmt::skip]
|
||||
pub mod packet_catalog;
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
//! Native LLSD field codecs for the translated Linden message fixtures.
|
||||
|
||||
use crate::messages::linden::{
|
||||
AgentGroupDataUpdateMessage, AgentGroupDataUpdateMessageGroupData,
|
||||
AgentGroupDataUpdateMessageNewGroupData, ChatSessionAcceptInvitation, ChatSessionRequestBlock,
|
||||
AgentDropGroupMessage, AgentDropGroupMessageAgentData, AgentGroupDataUpdateMessage,
|
||||
AgentGroupDataUpdateMessageGroupData, AgentGroupDataUpdateMessageNewGroupData,
|
||||
AgentStateUpdateMessage, BulkUpdateInventoryMessage, BulkUpdateInventoryMessageFolderDataInfo,
|
||||
BulkUpdateInventoryMessageItemDataInfo, ChatSessionAcceptInvitation, ChatSessionRequestBlock,
|
||||
ChatSessionRequestMuteUpdate, ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
|
||||
ChatterBoxSessionAgentListUpdatesMessage,
|
||||
ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock, ChatterBoxSessionStartReplyMessage,
|
||||
ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, DirLandReplyMessage,
|
||||
DirLandReplyMessageQueryReply, EnableSimulatorMessage,
|
||||
EnableSimulatorMessageSimulatorInfoBlock, EstablishAgentCommunicationMessage,
|
||||
ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage,
|
||||
DirLandReplyMessage, DirLandReplyMessageQueryReply, DisplayNameUpdateMessage,
|
||||
EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock,
|
||||
EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage,
|
||||
GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest,
|
||||
LandStatReplyMessage, LandStatReplyMessageReportDataBlock, MapLayerMessageBase,
|
||||
MapLayerReplyVariant, MapLayerReplyVariantLayerData, ParcelObjectOwnersReplyMessage,
|
||||
MapLayerReplyVariant, MapLayerReplyVariantLayerData, NavMeshStatusUpdateMessage,
|
||||
ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate,
|
||||
ObjectPhysicsPropertiesMessage, ParcelObjectOwnersReplyMessage,
|
||||
ParcelObjectOwnersReplyMessagePrimOwner, ParcelPropertiesMessage,
|
||||
ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage, PlacesReplyMessage,
|
||||
PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage, RemoteParcelRequestReply,
|
||||
RequiredVoiceVersionMessage, ScriptRunningReplyMessage, SendPostcardMessage,
|
||||
TeleportFailedMessage, TeleportFinishMessage, UpdateAgentInformationMessage,
|
||||
UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage,
|
||||
UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, ViewerStatsMessage,
|
||||
PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage, RegionInfoMessage,
|
||||
RemoteParcelRequestReply, RenderMaterialsMessage, RequiredVoiceVersionMessage,
|
||||
ScriptRunningReplyMessage, SearchStatRequestReply, SearchStatRequestRequest,
|
||||
SendPostcardMessage, SetDisplayNameMessage, SetDisplayNameReplyMessage,
|
||||
SimConsoleResponseMessage, TeleportFailedMessage, TeleportFinishMessage,
|
||||
UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage,
|
||||
UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, UploaderRequestComplete,
|
||||
UploaderRequestUpload, ViewerStatsMessage,
|
||||
};
|
||||
use crate::{
|
||||
Error, GroupPowers, InstantMessageDialog, InstantMessageOnline, LandingType, ParcelCategory,
|
||||
ParcelFlags, ParcelResult, ParcelStatus, SimAccess, TeleportFlags,
|
||||
AgentDisplayName, Error, GroupPowers, InstantMessageDialog, InstantMessageOnline, LandingType,
|
||||
MediaEntry, ParcelCategory, ParcelFlags, ParcelResult, ParcelStatus, PermissionMask,
|
||||
PrimitivePhysicsProperties, SimAccess, TeleportFlags,
|
||||
};
|
||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||
use flate2::read::ZlibDecoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::Uri;
|
||||
use libremetaverse_types::{UUID, Vector3};
|
||||
use libremetaverse_types::{AssetType, FolderType, InventoryType, SaleType, UUID, Vector3};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read as _;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -100,6 +112,18 @@ impl MessageValue for f32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for f64 {
|
||||
fn message_default() -> Self {
|
||||
0.0
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
OSD::from_real_with_double(*self)
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
value.as_real()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for String {
|
||||
fn message_default() -> Self {
|
||||
String::new()
|
||||
@@ -181,6 +205,73 @@ impl MessageValue for IpAddr {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for OSD {
|
||||
fn message_default() -> Self {
|
||||
Self::Undefined
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
Ok(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for OSDMap {
|
||||
fn message_default() -> Self {
|
||||
Self::new_with_constructor().expect("empty OSD map")
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.copy()
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
Self::new_with_dictionary(values.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for AgentDisplayName {
|
||||
fn message_default() -> Self {
|
||||
Self::new().expect("AgentDisplayName construction is infallible")
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.get_osd()
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
Self::from_osd(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for PrimitivePhysicsProperties {
|
||||
fn message_default() -> Self {
|
||||
Self::new().expect("PrimitivePhysicsProperties construction is infallible")
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.get_osd()
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
Self::from_osd(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for MediaEntry {
|
||||
fn message_default() -> Self {
|
||||
Self::new().expect("MediaEntry construction is infallible")
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
if self.is_undefined() {
|
||||
Ok(OSD::Undefined)
|
||||
} else {
|
||||
self.get_osd()?.copy()
|
||||
}
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
Self::from_osd(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! transparent_value {
|
||||
($type:ty, $inner:ty) => {
|
||||
impl MessageValue for $type {
|
||||
@@ -199,6 +290,21 @@ macro_rules! transparent_value {
|
||||
|
||||
transparent_value!(ParcelFlags, u32);
|
||||
transparent_value!(TeleportFlags, u32);
|
||||
transparent_value!(PermissionMask, u32);
|
||||
|
||||
impl MessageValue for InventoryType {
|
||||
fn message_default() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
OSD::from_integer_with_int32(i32::from(self.0))
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
Ok(Self(
|
||||
i8::try_from(value.as_integer()?).map_err(|_| Error::Argument)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for GroupPowers {
|
||||
fn message_default() -> Self {
|
||||
@@ -289,6 +395,88 @@ enum_value!(
|
||||
InstantMessageDialog::TaskInventoryOffered,
|
||||
]
|
||||
);
|
||||
enum_value!(
|
||||
SaleType,
|
||||
SaleType::Not,
|
||||
[
|
||||
SaleType::Contents,
|
||||
SaleType::Copy,
|
||||
SaleType::Not,
|
||||
SaleType::Original
|
||||
]
|
||||
);
|
||||
enum_value!(
|
||||
AssetType,
|
||||
AssetType::Unknown,
|
||||
[
|
||||
AssetType::Animation,
|
||||
AssetType::Bodypart,
|
||||
AssetType::CallingCard,
|
||||
AssetType::Clothing,
|
||||
AssetType::Folder,
|
||||
AssetType::Gesture,
|
||||
AssetType::ImageJPEG,
|
||||
AssetType::ImageTGA,
|
||||
AssetType::LSLBytecode,
|
||||
AssetType::LSLText,
|
||||
AssetType::Landmark,
|
||||
AssetType::Link,
|
||||
AssetType::LinkFolder,
|
||||
AssetType::Material,
|
||||
AssetType::Mesh,
|
||||
AssetType::Notecard,
|
||||
AssetType::Object,
|
||||
AssetType::Person,
|
||||
AssetType::Script,
|
||||
AssetType::Settings,
|
||||
AssetType::Simstate,
|
||||
AssetType::Sound,
|
||||
AssetType::SoundWAV,
|
||||
AssetType::Texture,
|
||||
AssetType::TextureTGA,
|
||||
AssetType::Unknown,
|
||||
AssetType::Widget
|
||||
]
|
||||
);
|
||||
enum_value!(
|
||||
FolderType,
|
||||
FolderType::None,
|
||||
[
|
||||
FolderType::Animation,
|
||||
FolderType::BasicRoot,
|
||||
FolderType::BodyPart,
|
||||
FolderType::CallingCard,
|
||||
FolderType::Clothing,
|
||||
FolderType::CurrentOutfit,
|
||||
FolderType::EnsembleEnd,
|
||||
FolderType::EnsembleStart,
|
||||
FolderType::Favorites,
|
||||
FolderType::Gesture,
|
||||
FolderType::Inbox,
|
||||
FolderType::LSLText,
|
||||
FolderType::Landmark,
|
||||
FolderType::LostAndFound,
|
||||
FolderType::MarketplaceListings,
|
||||
FolderType::MarketplaceStock,
|
||||
FolderType::MarketplaceVersion,
|
||||
FolderType::Material,
|
||||
FolderType::Mesh,
|
||||
FolderType::MyOutfits,
|
||||
FolderType::None,
|
||||
FolderType::Notecard,
|
||||
FolderType::Object,
|
||||
FolderType::OldRoot,
|
||||
FolderType::Outbox,
|
||||
FolderType::Outfit,
|
||||
FolderType::Root,
|
||||
FolderType::Settings,
|
||||
FolderType::Snapshot,
|
||||
FolderType::Sound,
|
||||
FolderType::Suitcase,
|
||||
FolderType::Texture,
|
||||
FolderType::Trash
|
||||
]
|
||||
);
|
||||
enum_value!(
|
||||
InstantMessageOnline,
|
||||
InstantMessageOnline::Online,
|
||||
@@ -444,6 +632,471 @@ fn integer_as_f32(value: &OSD) -> Result<f32, Error> {
|
||||
Ok(value.as_integer()? as f32)
|
||||
}
|
||||
|
||||
fn serialize_agent_state_update(value: &AgentStateUpdateMessage) -> Result<OSDMap, Error> {
|
||||
let access = map_osd([("max", value.max_access.to_message_osd()?)]);
|
||||
let preferences = map_osd([
|
||||
("access_prefs", access),
|
||||
(
|
||||
"alter_navmesh_objects",
|
||||
value.alter_navmesh_objects.to_message_osd()?,
|
||||
),
|
||||
(
|
||||
"alter_permanent_objects",
|
||||
value.alter_permanent_objects.to_message_osd()?,
|
||||
),
|
||||
("god_level", value.god_level.to_message_osd()?),
|
||||
("language", value.language.to_message_osd()?),
|
||||
(
|
||||
"language_is_public",
|
||||
value.language_is_public.to_message_osd()?,
|
||||
),
|
||||
]);
|
||||
finish_map([
|
||||
(
|
||||
"can_modify_navmesh",
|
||||
value.can_modify_navmesh.to_message_osd()?,
|
||||
),
|
||||
(
|
||||
"has_modified_navmesh",
|
||||
value.has_modified_navmesh.to_message_osd()?,
|
||||
),
|
||||
("preferences", preferences),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_agent_state_update(
|
||||
value: &mut AgentStateUpdateMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.raw_data = map.clone();
|
||||
value.can_modify_navmesh = map
|
||||
.get("can_modify_navmesh")
|
||||
.unwrap_or_default()
|
||||
.as_boolean()?;
|
||||
value.has_modified_navmesh = map
|
||||
.get("has_modified_navmesh")
|
||||
.unwrap_or_default()
|
||||
.as_boolean()?;
|
||||
let Some(OSD::Map(preferences)) = map.get("preferences") else {
|
||||
return Ok(());
|
||||
};
|
||||
let preferences = OSDMap::new_with_dictionary(preferences)?;
|
||||
value.alter_navmesh_objects = preferences
|
||||
.get("alter_navmesh_objects")
|
||||
.unwrap_or_default()
|
||||
.as_boolean()?;
|
||||
value.alter_permanent_objects = preferences
|
||||
.get("alter_permanent_objects")
|
||||
.unwrap_or_default()
|
||||
.as_boolean()?;
|
||||
value.god_level = preferences
|
||||
.get("god_level")
|
||||
.unwrap_or_default()
|
||||
.as_integer()?;
|
||||
value.language = preferences
|
||||
.get("language")
|
||||
.unwrap_or_default()
|
||||
.as_string()?;
|
||||
value.language_is_public = preferences
|
||||
.get("language_is_public")
|
||||
.unwrap_or_default()
|
||||
.as_boolean()?;
|
||||
if let Some(OSD::Map(access)) = preferences.get("access_prefs") {
|
||||
value.max_access = access.get("max").unwrap_or(&OSD::Undefined).as_string()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_crossed_region(value: &CrossedRegionMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
(
|
||||
"Info",
|
||||
one_block_osd([
|
||||
("LookAt", value.look_at.to_message_osd()?),
|
||||
("Position", value.position.to_message_osd()?),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"AgentData",
|
||||
one_block_osd([
|
||||
("AgentID", value.agent_id.to_message_osd()?),
|
||||
("SessionID", value.session_id.to_message_osd()?),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"RegionData",
|
||||
one_block_osd([
|
||||
("RegionHandle", value.region_handle.to_message_osd()?),
|
||||
("SeedCapability", value.seed_capability.to_message_osd()?),
|
||||
("SimIP", value.ip.to_message_osd()?),
|
||||
("SimPort", value.port.to_message_osd()?),
|
||||
("RegionSizeX", value.region_size_x.to_message_osd()?),
|
||||
("RegionSizeY", value.region_size_y.to_message_osd()?),
|
||||
]),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn first_block(map: &OSDMap, key: &str) -> Result<OSDMap, Error> {
|
||||
let encoded = required(map, key)?;
|
||||
let blocks = as_array(&encoded)?;
|
||||
let block = as_map(blocks.first().ok_or(Error::Argument)?)?;
|
||||
OSDMap::new_with_dictionary(block.clone())
|
||||
}
|
||||
|
||||
fn deserialize_crossed_region(value: &mut CrossedRegionMessage, map: &OSDMap) -> Result<(), Error> {
|
||||
let info = first_block(map, "Info")?;
|
||||
value.look_at = decode_top(&info, "LookAt")?;
|
||||
value.position = decode_top(&info, "Position")?;
|
||||
let agent = first_block(map, "AgentData")?;
|
||||
value.agent_id = decode_top(&agent, "AgentID")?;
|
||||
value.session_id = decode_top(&agent, "SessionID")?;
|
||||
let region = first_block(map, "RegionData")?;
|
||||
value.region_handle = decode_top(®ion, "RegionHandle")?;
|
||||
value.seed_capability = decode_top(®ion, "SeedCapability")?;
|
||||
value.ip = decode_top(®ion, "SimIP")?;
|
||||
value.port = decode_top(®ion, "SimPort")?;
|
||||
if region.get("RegionSizeX").is_some() {
|
||||
value.region_size_x = decode_top(®ion, "RegionSizeX")?;
|
||||
}
|
||||
if region.get("RegionSizeY").is_some() {
|
||||
value.region_size_y = decode_top(®ion, "RegionSizeY")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_get_object_cost(value: &GetObjectCostMessage) -> Result<OSDMap, Error> {
|
||||
let values = HashMap::from([
|
||||
(
|
||||
"linked_set_resource_cost".to_owned(),
|
||||
OSD::Real(value.link_cost),
|
||||
),
|
||||
("resource_cost".to_owned(), OSD::Real(value.object_cost)),
|
||||
("physics_cost".to_owned(), OSD::Real(value.physics_cost)),
|
||||
(
|
||||
"linked_set_physics_cost".to_owned(),
|
||||
OSD::Real(value.link_physics_cost),
|
||||
),
|
||||
]);
|
||||
OSDMap::new_with_dictionary(HashMap::from([(
|
||||
value.object_id.to_string(),
|
||||
OSD::Map(values),
|
||||
)]))
|
||||
}
|
||||
|
||||
fn deserialize_get_object_cost(
|
||||
value: &mut GetObjectCostMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let mut entries: Vec<_> = map.snapshot().into_iter().collect();
|
||||
entries.sort_unstable_by(|left, right| left.0.cmp(&right.0));
|
||||
let Some((id, OSD::Map(costs))) = entries.into_iter().next() else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
value.object_id = UUID::new_with_string(id).unwrap_or_else(|_| UUID::zero());
|
||||
value.link_cost = costs
|
||||
.get("linked_set_resource_cost")
|
||||
.unwrap_or(&OSD::Undefined)
|
||||
.as_real()?;
|
||||
value.object_cost = costs
|
||||
.get("resource_cost")
|
||||
.unwrap_or(&OSD::Undefined)
|
||||
.as_real()?;
|
||||
value.physics_cost = costs
|
||||
.get("physics_cost")
|
||||
.unwrap_or(&OSD::Undefined)
|
||||
.as_real()?;
|
||||
value.link_physics_cost = costs
|
||||
.get("linked_set_physics_cost")
|
||||
.unwrap_or(&OSD::Undefined)
|
||||
.as_real()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_nav_mesh_status(value: &NavMeshStatusUpdateMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("region_id", value.region_id.to_message_osd()?),
|
||||
("status", value.status.to_message_osd()?),
|
||||
("version", value.version.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_nav_mesh_status(
|
||||
value: &mut NavMeshStatusUpdateMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.raw_data = map.clone();
|
||||
value.region_id = decode_top(map, "region_id")?;
|
||||
value.status = decode_top(map, "status")?;
|
||||
value.version = decode_top(map, "version")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_region_info(value: &RegionInfoMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("parcel_local_id", value.parcel_local_id.to_message_osd()?),
|
||||
("region_name", value.region_name.to_message_osd()?),
|
||||
(
|
||||
"voice_credentials",
|
||||
map_osd([("channel_uri", value.channel_uri.to_message_osd()?)]),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_region_info(value: &mut RegionInfoMessage, map: &OSDMap) -> Result<(), Error> {
|
||||
value.parcel_local_id = decode_top(map, "parcel_local_id")?;
|
||||
value.region_name = decode_top(map, "region_name")?;
|
||||
let OSD::Map(voice) = required(map, "voice_credentials")? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
value.channel_uri = voice
|
||||
.get("channel_uri")
|
||||
.unwrap_or(&OSD::Undefined)
|
||||
.as_string()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_render_materials(_value: &RenderMaterialsMessage) -> Result<OSDMap, Error> {
|
||||
OSDMap::new_with_constructor()
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)] // GeneratedMessage requires a fallible codec signature.
|
||||
fn deserialize_render_materials(
|
||||
value: &mut RenderMaterialsMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let decoded = map
|
||||
.get("Zipped")
|
||||
.ok_or(Error::Argument)
|
||||
.and_then(|value| value.as_binary())
|
||||
.and_then(|bytes| {
|
||||
let mut output = Vec::new();
|
||||
ZlibDecoder::new(bytes.as_slice())
|
||||
.read_to_end(&mut output)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
OSDParser::deserialize_llsd_binary_with_bytes(output)
|
||||
});
|
||||
value.material_data = decoded.unwrap_or_else(|_| OSD::Map(HashMap::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_set_display_name(value: &SetDisplayNameMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([(
|
||||
"display_name",
|
||||
OSD::Array(vec![
|
||||
OSD::String(value.old_display_name.clone()),
|
||||
OSD::String(value.new_display_name.clone()),
|
||||
]),
|
||||
)])
|
||||
}
|
||||
|
||||
fn deserialize_set_display_name(
|
||||
value: &mut SetDisplayNameMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let OSD::Array(names) = required(map, "display_name")? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
value.old_display_name = names.first().unwrap_or(&OSD::Undefined).as_string()?;
|
||||
value.new_display_name = names.get(1).unwrap_or(&OSD::Undefined).as_string()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_set_display_name_reply(value: &SetDisplayNameReplyMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("content", value.display_name.to_message_osd()?),
|
||||
("reason", value.reason.to_message_osd()?),
|
||||
("status", value.status.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_set_display_name_reply(
|
||||
value: &mut SetDisplayNameReplyMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.display_name = decode_top(map, "content")?;
|
||||
value.reason = decode_top(map, "reason")?;
|
||||
value.status = decode_top(map, "status")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_display_name_update(value: &DisplayNameUpdateMessage) -> Result<OSDMap, Error> {
|
||||
let OSD::Map(mut agent) = value.display_name.to_message_osd()? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
agent.insert(
|
||||
"old_display_name".into(),
|
||||
OSD::String(value.old_display_name.clone()),
|
||||
);
|
||||
finish_map([("agent", OSD::Map(agent))])
|
||||
}
|
||||
|
||||
fn deserialize_display_name_update(
|
||||
value: &mut DisplayNameUpdateMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let agent = required(map, "agent")?;
|
||||
let OSD::Map(agent_map) = &agent else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
value.display_name = AgentDisplayName::from_osd(agent.clone())?;
|
||||
value.old_display_name = agent_map
|
||||
.get("old_display_name")
|
||||
.unwrap_or(&OSD::Undefined)
|
||||
.as_string()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_uploader_upload(value: &UploaderRequestUpload) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("state", OSD::String("upload".into())),
|
||||
("uploader", value.url.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_uploader_upload(
|
||||
value: &mut UploaderRequestUpload,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
if required(map, "state")?.as_string()? != "upload" {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
value.url = decode_top(map, "uploader")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_uploader_complete(value: &UploaderRequestComplete) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("state", OSD::String("complete".into())),
|
||||
("new_asset", value.asset_id.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_uploader_complete(
|
||||
value: &mut UploaderRequestComplete,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
if required(map, "state")?.as_string()? != "complete" {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
value.asset_id = decode_top(map, "new_asset")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_bulk_folder(
|
||||
value: &BulkUpdateInventoryMessageFolderDataInfo,
|
||||
) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("FolderID", value.folder_id.to_message_osd()?),
|
||||
("ParentID", value.parent_id.to_message_osd()?),
|
||||
("Name", value.name.to_message_osd()?),
|
||||
("Type", value.type_.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_bulk_folder(
|
||||
value: &mut BulkUpdateInventoryMessageFolderDataInfo,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.folder_id = decode_top(map, "FolderID")?;
|
||||
value.parent_id = decode_top(map, "ParentID")?;
|
||||
value.name = decode_top(map, "Name")?;
|
||||
value.type_ = decode_top(map, "Type")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_bulk_item(value: &BulkUpdateInventoryMessageItemDataInfo) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("ItemID", value.item_id.to_message_osd()?),
|
||||
("CallbackID", value.callback_id.to_message_osd()?),
|
||||
("FolderID", value.folder_id.to_message_osd()?),
|
||||
("CreatorID", value.creator_id.to_message_osd()?),
|
||||
("OwnerID", value.owner_id.to_message_osd()?),
|
||||
("GroupID", value.group_id.to_message_osd()?),
|
||||
("BaseMask", value.base_mask.to_message_osd()?),
|
||||
("OwnerMask", value.owner_mask.to_message_osd()?),
|
||||
("GroupMask", value.group_mask.to_message_osd()?),
|
||||
("EveryoneMask", value.everyone_mask.to_message_osd()?),
|
||||
("NextOwnerMask", value.next_owner_mask.to_message_osd()?),
|
||||
("GroupOwned", value.group_owned.to_message_osd()?),
|
||||
("AssetID", value.asset_id.to_message_osd()?),
|
||||
("Type", value.type_.to_message_osd()?),
|
||||
("InvType", value.inv_type.to_message_osd()?),
|
||||
("Flags", value.flags.to_message_osd()?),
|
||||
("SaleType", value.sale_type.to_message_osd()?),
|
||||
("SalePrice", value.sale_price.to_message_osd()?),
|
||||
("Name", value.name.to_message_osd()?),
|
||||
("Description", value.description.to_message_osd()?),
|
||||
(
|
||||
"CreationDate",
|
||||
unix_seconds(value.creation_date).to_message_osd()?,
|
||||
),
|
||||
("CRC", value.crc.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_bulk_item(
|
||||
value: &mut BulkUpdateInventoryMessageItemDataInfo,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.item_id = decode_top(map, "ItemID")?;
|
||||
value.callback_id = decode_top(map, "CallbackID")?;
|
||||
value.folder_id = decode_top(map, "FolderID")?;
|
||||
value.creator_id = decode_top(map, "CreatorID")?;
|
||||
value.owner_id = decode_top(map, "OwnerID")?;
|
||||
value.group_id = decode_top(map, "GroupID")?;
|
||||
value.base_mask = decode_top(map, "BaseMask")?;
|
||||
value.owner_mask = decode_top(map, "OwnerMask")?;
|
||||
value.group_mask = decode_top(map, "GroupMask")?;
|
||||
value.everyone_mask = decode_top(map, "EveryoneMask")?;
|
||||
value.next_owner_mask = decode_top(map, "NextOwnerMask")?;
|
||||
value.group_owned = decode_top(map, "GroupOwned")?;
|
||||
value.asset_id = decode_top(map, "AssetID")?;
|
||||
value.type_ = decode_top(map, "Type")?;
|
||||
value.inv_type = decode_top(map, "InvType")?;
|
||||
value.flags = decode_top(map, "Flags")?;
|
||||
value.sale_type = decode_top(map, "SaleType")?;
|
||||
// Preserve the reference adapter's field selection for wire compatibility.
|
||||
value.sale_price = decode_top(map, "SaleType")?;
|
||||
value.name = decode_top(map, "Name")?;
|
||||
value.description = decode_top(map, "Description")?;
|
||||
value.creation_date = time_from_unix(decode_top(map, "CreationDate")?);
|
||||
value.crc = decode_top(map, "CRC")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_bulk_inventory(value: &BulkUpdateInventoryMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
(
|
||||
"AgentData",
|
||||
one_block_osd([
|
||||
("AgentID", value.agent_id.to_message_osd()?),
|
||||
("TransactionID", value.transaction_id.to_message_osd()?),
|
||||
]),
|
||||
),
|
||||
("FolderData", encode_struct_array(&value.folder_data)?),
|
||||
("ItemData", encode_struct_array(&value.item_data)?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_bulk_inventory(
|
||||
value: &mut BulkUpdateInventoryMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let agent = first_block(map, "AgentData")?;
|
||||
value.agent_id = decode_top(&agent, "AgentID")?;
|
||||
value.transaction_id = decode_top(&agent, "TransactionID")?;
|
||||
value.folder_data = match map.get("FolderData") {
|
||||
Some(encoded) => decode_array(&encoded)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
value.item_data = match map.get("ItemData") {
|
||||
Some(encoded) => decode_array(&encoded)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! map_message {
|
||||
($type:ty { scalar: [$($scalar:ident => $scalar_key:literal),* $(,)?], array: [$($array:ident => $array_key:literal),* $(,)?], bytes: [$($bytes:ident => $bytes_key:literal),* $(,)?] } $(init $init:expr)?) => {
|
||||
impl GeneratedMessage for $type {
|
||||
@@ -549,6 +1202,36 @@ map_message!(AgentGroupDataUpdateMessageNewGroupData {
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(AgentDropGroupMessageAgentData {
|
||||
scalar: [agent_id => "AgentID", group_id => "GroupID"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(AgentDropGroupMessage {
|
||||
scalar: [],
|
||||
array: [agent_data_block => "AgentData"],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(BulkUpdateInventoryMessageFolderDataInfo {
|
||||
scalar: [folder_id, name, parent_id, type_],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_bulk_folder, deserialize_bulk_folder);
|
||||
custom_message!(BulkUpdateInventoryMessageItemDataInfo {
|
||||
scalar: [asset_id, base_mask, crc, callback_id, creation_date, creator_id, description, everyone_mask, flags, folder_id, group_id, group_mask, group_owned, inv_type, item_id, name, next_owner_mask, owner_id, owner_mask, sale_price, sale_type, type_],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_bulk_item, deserialize_bulk_item);
|
||||
custom_message!(BulkUpdateInventoryMessage {
|
||||
scalar: [agent_id, transaction_id],
|
||||
array: [folder_data, item_data],
|
||||
bytes: []
|
||||
} => serialize_bulk_inventory, deserialize_bulk_inventory);
|
||||
custom_message!(AgentStateUpdateMessage {
|
||||
scalar: [alter_navmesh_objects, alter_permanent_objects, can_modify_navmesh, god_level, has_modified_navmesh, language, language_is_public, max_access, raw_data],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_agent_state_update, deserialize_agent_state_update);
|
||||
custom_message!(AgentGroupDataUpdateMessage {
|
||||
scalar: [agent_id, avatar_id],
|
||||
array: [group_data_block, new_group_data_block],
|
||||
@@ -583,6 +1266,14 @@ custom_message!(ChatterBoxInvitationMessage {
|
||||
array: [],
|
||||
bytes: [binary_bucket]
|
||||
} => serialize_chatter_invitation, deserialize_chatter_invitation);
|
||||
custom_message!(CrossedRegionMessage {
|
||||
scalar: [agent_id, ip, look_at, port, position, region_handle, region_size_x, region_size_y, seed_capability, session_id],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_crossed_region, deserialize_crossed_region, init |value: &mut CrossedRegionMessage| {
|
||||
value.region_size_x = crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X;
|
||||
value.region_size_y = crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y;
|
||||
});
|
||||
map_message!(ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock {
|
||||
scalar: [
|
||||
agent_id => "agent_id",
|
||||
@@ -859,6 +1550,96 @@ map_message!(RemoteParcelRequestReply {
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(SearchStatRequestRequest {
|
||||
scalar: [classified_id => "classified_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(SearchStatRequestReply {
|
||||
scalar: [
|
||||
map_clicks => "map_clicks",
|
||||
profile_clicks => "profile_clicks",
|
||||
search_map_clicks => "search_map_clicks",
|
||||
search_profile_clicks => "search_profile_clicks",
|
||||
search_teleport_clicks => "search_teleport_clicks",
|
||||
teleport_clicks => "teleport_clicks"
|
||||
],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(ForceCloseChatterBoxSessionMessage {
|
||||
scalar: [reason => "reason", session_id => "session_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(GetDisplayNamesMessage {
|
||||
scalar: [],
|
||||
array: [agents => "agents", bad_i_ds => "bad_ids"],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(GetObjectCostMessage {
|
||||
scalar: [link_cost, link_physics_cost, object_cost, object_id, physics_cost],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_get_object_cost, deserialize_get_object_cost);
|
||||
map_message!(GetObjectCostRequest {
|
||||
scalar: [],
|
||||
array: [object_i_ds => "object_ids"],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(LandResourcesMessage {
|
||||
scalar: [
|
||||
script_resource_details => "ScriptResourceDetails",
|
||||
script_resource_summary => "ScriptResourceSummary"
|
||||
],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(LandResourcesRequest {
|
||||
scalar: [parcel_id => "parcel_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(NavMeshStatusUpdateMessage {
|
||||
scalar: [raw_data, region_id, status, version],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_nav_mesh_status, deserialize_nav_mesh_status);
|
||||
map_message!(ObjectMediaNavigateMessage {
|
||||
scalar: [face => "texture_index", prim_id => "object_id", url => "current_url"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(ObjectMediaRequest {
|
||||
scalar: [prim_id => "object_id", verb => "verb"],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut ObjectMediaRequest| value.verb = "GET".into());
|
||||
map_message!(ObjectMediaResponse {
|
||||
scalar: [prim_id => "object_id", version => "object_media_version"],
|
||||
array: [face_media => "object_media_data"],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(ObjectMediaUpdate {
|
||||
scalar: [prim_id => "object_id", verb => "verb"],
|
||||
array: [face_media => "object_media_data"],
|
||||
bytes: []
|
||||
} init |value: &mut ObjectMediaUpdate| value.verb = "UPDATE".into());
|
||||
map_message!(ObjectPhysicsPropertiesMessage {
|
||||
scalar: [],
|
||||
array: [object_physics_properties => "ObjectData"],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(RegionInfoMessage {
|
||||
scalar: [channel_uri, parcel_local_id, region_name],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_region_info, deserialize_region_info);
|
||||
custom_message!(RenderMaterialsMessage {
|
||||
scalar: [material_data],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_render_materials, deserialize_render_materials);
|
||||
map_message!(RequiredVoiceVersionMessage {
|
||||
scalar: [
|
||||
major_version => "major_version",
|
||||
@@ -884,6 +1665,26 @@ map_message!(SendPostcardMessage {
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(SetDisplayNameMessage {
|
||||
scalar: [new_display_name, old_display_name],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_set_display_name, deserialize_set_display_name);
|
||||
custom_message!(SetDisplayNameReplyMessage {
|
||||
scalar: [display_name, reason, status],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_set_display_name_reply, deserialize_set_display_name_reply);
|
||||
custom_message!(DisplayNameUpdateMessage {
|
||||
scalar: [display_name, old_display_name],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_display_name_update, deserialize_display_name_update);
|
||||
map_message!(SimConsoleResponseMessage {
|
||||
scalar: [body => "body"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(TeleportFailedMessage {
|
||||
scalar: [agent_id, extra_params, message_key, reason],
|
||||
array: [],
|
||||
@@ -982,6 +1783,16 @@ custom_message!(ViewerStatsMessage {
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_viewer_stats, deserialize_viewer_stats);
|
||||
custom_message!(UploaderRequestUpload {
|
||||
scalar: [url],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_upload, deserialize_uploader_upload);
|
||||
custom_message!(UploaderRequestComplete {
|
||||
scalar: [asset_id],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_complete, deserialize_uploader_complete);
|
||||
|
||||
fn serialize_agent_group(value: &AgentGroupDataUpdateMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
|
||||
1267
crates/libremetaverse/src/message_decoder.rs
Normal file
1267
crates/libremetaverse/src/message_decoder.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -49,14 +49,6 @@ impl Clone for Packet {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Caps {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
simulator: self.simulator.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
@@ -236,6 +228,23 @@ pub struct SimConnectedEventArgs {
|
||||
simulator: Simulator,
|
||||
}
|
||||
|
||||
/// Event arguments raised when a simulator's `EventQueueGet` poller is live.
|
||||
#[derive(Clone)]
|
||||
pub struct EventQueueRunningEventArgs {
|
||||
simulator: Simulator,
|
||||
}
|
||||
|
||||
impl EventQueueRunningEventArgs {
|
||||
pub const fn new(simulator: Simulator) -> Result<Self, Error> {
|
||||
Ok(Self { simulator })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn simulator(&self) -> Simulator {
|
||||
self.simulator.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl SimConnectedEventArgs {
|
||||
pub fn new(simulator: Simulator) -> Result<Self, Error> {
|
||||
Ok(Self { simulator })
|
||||
@@ -849,6 +858,9 @@ pub struct SimulatorData {
|
||||
disconnect_candidate: AtomicBool,
|
||||
circuit_code: AtomicU32,
|
||||
seed_caps: RwLock<Option<Uri>>,
|
||||
caps_state: RwLock<Option<Arc<crate::caps::CapsInner>>>,
|
||||
event_queue_generation: Mutex<u64>,
|
||||
event_queue_wait: Condvar,
|
||||
manager: Mutex<Weak<NetworkManagerInner>>,
|
||||
transport: UDPBase,
|
||||
transport_thread: Mutex<Option<TransportThread>>,
|
||||
@@ -858,6 +870,14 @@ pub struct SimulatorData {
|
||||
|
||||
impl Drop for SimulatorData {
|
||||
fn drop(&mut self) {
|
||||
if let Some(caps) = self
|
||||
.caps_state
|
||||
.get_mut()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take()
|
||||
{
|
||||
caps.disconnect(true);
|
||||
}
|
||||
let runtime = self
|
||||
.transport_thread
|
||||
.get_mut()
|
||||
@@ -873,7 +893,6 @@ impl Drop for SimulatorData {
|
||||
}
|
||||
|
||||
/// Reference-identity simulator handle.
|
||||
#[derive(Clone)]
|
||||
pub struct Simulator {
|
||||
/// Capability client associated with this simulator.
|
||||
///
|
||||
@@ -883,6 +902,19 @@ pub struct Simulator {
|
||||
data: Arc<SimulatorData>,
|
||||
}
|
||||
|
||||
impl Clone for Simulator {
|
||||
fn clone(&self) -> Self {
|
||||
let mut clone = self.native_clone_without_caps();
|
||||
if let Some(inner) = read(&self.data.caps_state).clone() {
|
||||
clone.caps = Some(Box::new(Caps::native_from_inner(
|
||||
inner,
|
||||
clone.native_clone_without_caps(),
|
||||
)));
|
||||
}
|
||||
clone
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Simulator {
|
||||
type Target = SimulatorData;
|
||||
|
||||
@@ -987,6 +1019,9 @@ impl Simulator {
|
||||
disconnect_candidate: AtomicBool::new(false),
|
||||
circuit_code: AtomicU32::new(0),
|
||||
seed_caps: RwLock::new(None),
|
||||
caps_state: RwLock::new(None),
|
||||
event_queue_generation: Mutex::new(0),
|
||||
event_queue_wait: Condvar::new(),
|
||||
manager: Mutex::new(Weak::new()),
|
||||
transport,
|
||||
transport_thread: Mutex::new(None),
|
||||
@@ -997,6 +1032,29 @@ impl Simulator {
|
||||
Ok(Self { caps: None, data })
|
||||
}
|
||||
|
||||
pub(crate) fn native_clone_without_caps(&self) -> Self {
|
||||
Self {
|
||||
caps: None,
|
||||
data: Arc::clone(&self.data),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_data_weak(&self) -> Weak<SimulatorData> {
|
||||
Arc::downgrade(&self.data)
|
||||
}
|
||||
|
||||
pub(crate) fn native_data_arc(&self) -> Arc<SimulatorData> {
|
||||
Arc::clone(&self.data)
|
||||
}
|
||||
|
||||
pub(crate) fn native_from_weak(data: &Weak<SimulatorData>) -> Option<Self> {
|
||||
data.upgrade().map(|data| Self { caps: None, data })
|
||||
}
|
||||
|
||||
pub(crate) fn native_from_data(data: Arc<SimulatorData>) -> Self {
|
||||
Self { caps: None, data }
|
||||
}
|
||||
|
||||
fn attach_manager(&self, manager: &Arc<NetworkManagerInner>, circuit_code: u32) {
|
||||
*mutex(&self.manager) = Arc::downgrade(manager);
|
||||
self.circuit_code.store(circuit_code, Ordering::Release);
|
||||
@@ -1084,6 +1142,10 @@ impl Simulator {
|
||||
}
|
||||
|
||||
pub fn native_disconnect(&self, send_close_circuit: bool) -> Result<(), Error> {
|
||||
if let Some(caps) = write(&self.caps_state).take() {
|
||||
caps.disconnect(true);
|
||||
}
|
||||
*write(&self.seed_caps) = None;
|
||||
self.disconnect_candidate.store(false, Ordering::Release);
|
||||
let was_connected = self.connected.swap(false, Ordering::AcqRel);
|
||||
self.handshake_complete.store(false, Ordering::Release);
|
||||
@@ -1198,14 +1260,114 @@ impl Simulator {
|
||||
seed_caps: Option<Uri>,
|
||||
changed_sim: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
let mut current = write(&self.seed_caps);
|
||||
if !changed_sim.unwrap_or(false) && *current == seed_caps {
|
||||
if !changed_sim.unwrap_or(false) && *read(&self.seed_caps) == seed_caps {
|
||||
return Ok(());
|
||||
}
|
||||
*current = seed_caps;
|
||||
if let Some(previous) = write(&self.caps_state).take() {
|
||||
previous.disconnect(true);
|
||||
}
|
||||
write(&self.seed_caps).clone_from(&seed_caps);
|
||||
if let Some(seed_caps) = seed_caps {
|
||||
let caps = Caps::native_create(self, seed_caps)?;
|
||||
*write(&self.caps_state) = Some(caps);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn native_is_event_queue_running(
|
||||
&self,
|
||||
block_until_running: Option<bool>,
|
||||
) -> Result<bool, Error> {
|
||||
if self.caps_running() {
|
||||
return Ok(true);
|
||||
}
|
||||
if !block_until_running.unwrap_or(false) {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut generation = mutex(&self.event_queue_generation);
|
||||
for delay in [1_u64, 2, 4, 8] {
|
||||
if self.caps_running() || self.current_sim_caps_running() {
|
||||
return Ok(true);
|
||||
}
|
||||
self.start_current_event_queue();
|
||||
let observed = *generation;
|
||||
let (next, _) = self
|
||||
.event_queue_wait
|
||||
.wait_timeout_while(generation, Duration::from_secs(delay), |value| {
|
||||
*value == observed
|
||||
&& !self.client.cancellation_token().is_cancellation_requested()
|
||||
})
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
generation = next;
|
||||
if *generation != observed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(self.caps_running() || self.current_sim_caps_running())
|
||||
}
|
||||
|
||||
fn caps_running(&self) -> bool {
|
||||
read(&self.caps_state).as_ref().is_some_and(|inner| {
|
||||
Caps::native_from_inner(Arc::clone(inner), self.native_clone_without_caps())
|
||||
.is_event_queue_running()
|
||||
})
|
||||
}
|
||||
|
||||
fn current_sim_caps_running(&self) -> bool {
|
||||
mutex(&self.manager)
|
||||
.upgrade()
|
||||
.and_then(|manager| read(&manager.current_sim).clone())
|
||||
.is_some_and(|simulator| simulator.caps_running())
|
||||
}
|
||||
|
||||
fn start_current_event_queue(&self) {
|
||||
let simulator = mutex(&self.manager)
|
||||
.upgrade()
|
||||
.and_then(|manager| read(&manager.current_sim).clone())
|
||||
.unwrap_or_else(|| self.native_clone_without_caps());
|
||||
if let Some(inner) = read(&simulator.caps_state).clone() {
|
||||
let caps = Caps::native_from_inner(inner, simulator.native_clone_without_caps());
|
||||
if let Some(queue) = caps.event_queue() {
|
||||
let _ = queue.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_raise_event_queue_running(&self) {
|
||||
let mut generation = mutex(&self.event_queue_generation);
|
||||
*generation = generation.wrapping_add(1);
|
||||
drop(generation);
|
||||
self.event_queue_wait.notify_all();
|
||||
if let Some(manager) = mutex(&self.manager).upgrade() {
|
||||
let args = EventQueueRunningEventArgs {
|
||||
simulator: self.clone(),
|
||||
};
|
||||
manager
|
||||
.events
|
||||
.event_queue_running
|
||||
.emit_with(|| args.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_dispatch_caps_event(&self, event_name: &str, message: &dyn IMessage) {
|
||||
if let Some(manager) = mutex(&self.manager).upgrade() {
|
||||
manager
|
||||
.caps_events
|
||||
.invoke_raise_event(event_name, message, self.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_enqueue_caps_packet(&self, packet: Packet) -> Result<(), Error> {
|
||||
let manager = mutex(&self.manager)
|
||||
.upgrade()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
manager.enqueue_incoming(NetworkManagerIncomingPacket {
|
||||
packet: Some(packet),
|
||||
simulator: Some(self.clone()),
|
||||
raw_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn seed_capability(&self) -> Option<Uri> {
|
||||
read(&self.seed_caps).clone()
|
||||
@@ -1262,6 +1424,11 @@ impl Simulator {
|
||||
self.connected.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn native_set_connected_for_tests(&self, connected: bool) {
|
||||
self.connected.store(connected, Ordering::Release);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_handshake_complete(&self) -> bool {
|
||||
self.handshake_complete.load(Ordering::Acquire)
|
||||
@@ -1397,6 +1564,7 @@ struct NetworkWorkers {
|
||||
#[derive(Default)]
|
||||
struct NetworkEvents {
|
||||
disconnected: EventRegistry<DisconnectedEventArgs>,
|
||||
event_queue_running: EventRegistry<EventQueueRunningEventArgs>,
|
||||
generic_streaming_message: EventRegistry<GenericStreamingMessageEventArgs>,
|
||||
packet_sent: EventRegistry<PacketSentEventArgs>,
|
||||
sim_changed: EventRegistry<SimChangedEventArgs>,
|
||||
@@ -1944,6 +2112,13 @@ impl NetworkManager {
|
||||
self.inner.events.disconnected.subscribe(handler)
|
||||
}
|
||||
|
||||
pub fn native_subscribe_event_queue_running(
|
||||
&self,
|
||||
handler: EventHandler<EventQueueRunningEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.events.event_queue_running.subscribe(handler)
|
||||
}
|
||||
|
||||
pub fn native_subscribe_generic_streaming_message(
|
||||
&self,
|
||||
handler: EventHandler<GenericStreamingMessageEventArgs>,
|
||||
@@ -2254,6 +2429,8 @@ impl NetworkManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn manager_workers_and_callback_registries_do_not_retain_the_client() {
|
||||
@@ -2269,4 +2446,82 @@ mod tests {
|
||||
|
||||
assert!(probe.is_released());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_packet_fallback_uses_the_bounded_udp_dispatch_pipeline() {
|
||||
let client = GridClient::new().expect("client");
|
||||
let manager = NetworkManager::new(client.clone()).expect("network manager");
|
||||
manager.inner.ensure_workers().expect("workers");
|
||||
let simulator =
|
||||
Simulator::new(client, "127.0.0.1:14000".parse().unwrap(), 10, None, None).unwrap();
|
||||
simulator.attach_manager(&manager.inner, 0);
|
||||
manager.simulators.push_if_missing(simulator.clone());
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let _subscription = manager.subscribe_packet(
|
||||
PacketType::PacketAck,
|
||||
Arc::new(move |args| {
|
||||
sender
|
||||
.send((args.packet().type_, args.simulator().handle))
|
||||
.unwrap();
|
||||
}),
|
||||
false,
|
||||
);
|
||||
let body = OSDMap::new_with_constructor().unwrap();
|
||||
body.add_with_string_osd("Packets".to_owned(), OSD::Array(Vec::new()))
|
||||
.unwrap();
|
||||
let packet = Packet::build_packet_with_string_osd_map("PacketAck".to_owned(), body)
|
||||
.unwrap()
|
||||
.expect("packet fallback");
|
||||
simulator.native_enqueue_caps_packet(packet).unwrap();
|
||||
assert_eq!(
|
||||
receiver.recv_timeout(Duration::from_secs(2)).unwrap(),
|
||||
(PacketType::PacketAck, 10)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_queue_running_and_typed_caps_events_use_manager_registries() {
|
||||
let client = GridClient::new().expect("client");
|
||||
let manager = NetworkManager::new(client.clone()).expect("network manager");
|
||||
let simulator =
|
||||
Simulator::new(client, "127.0.0.1:14001".parse().unwrap(), 11, None, None).unwrap();
|
||||
simulator.attach_manager(&manager.inner, 0);
|
||||
|
||||
let (running_sender, running_receiver) = mpsc::channel();
|
||||
let _running = manager.native_subscribe_event_queue_running(Arc::new(move |args| {
|
||||
running_sender.send(args.simulator().handle).unwrap();
|
||||
}));
|
||||
simulator.native_raise_event_queue_running();
|
||||
assert_eq!(
|
||||
running_receiver
|
||||
.recv_timeout(Duration::from_secs(1))
|
||||
.unwrap(),
|
||||
11
|
||||
);
|
||||
|
||||
let (caps_sender, caps_receiver) = mpsc::channel();
|
||||
let _caps = manager.subscribe_caps(
|
||||
"UpdateAgentLanguage".to_owned(),
|
||||
CapsEventQueueCallback::from_handler(move |name, message, simulator| {
|
||||
let typed = (message as &dyn std::any::Any)
|
||||
.downcast_ref::<crate::messages::linden::UpdateAgentLanguageMessage>();
|
||||
caps_sender
|
||||
.send((name.to_owned(), typed.is_some(), simulator.handle))
|
||||
.unwrap();
|
||||
}),
|
||||
);
|
||||
let body = OSDMap::new_with_dictionary(HashMap::from([
|
||||
("language".to_owned(), OSD::String("en".to_owned())),
|
||||
("language_is_public".to_owned(), OSD::Boolean(true)),
|
||||
]))
|
||||
.unwrap();
|
||||
let message = crate::message_decoder::decode_event("UpdateAgentLanguage", &body)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
simulator.native_dispatch_caps_event("UpdateAgentLanguage", message.as_ref());
|
||||
assert_eq!(
|
||||
caps_receiver.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||
("UpdateAgentLanguage".to_owned(), true, 11)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Capability HTTP, rate limiting, and downloads
|
||||
# Capability HTTP, event queues, rate limiting, and downloads
|
||||
|
||||
`HttpCapsClient`, `CapsRateLimiter`, and `Http::DownloadManager` are native Rust
|
||||
implementations of the corresponding LibreMetaverse classes. They do not call
|
||||
@@ -7,6 +7,27 @@ client using rustls, its category limiter, and their shutdown order. Tests and
|
||||
embedders can inject either a `reqwest::Client` or an executor-neutral
|
||||
`HttpMessageHandler`, plus a `TimeProvider` for deterministic token-bucket time.
|
||||
|
||||
## Seed capabilities and EventQueueGet
|
||||
|
||||
`Simulator::SetSeedCaps` owns a native `Caps` instance. It posts the complete
|
||||
reference capability-name array as LLSD/XML, accepts only HTTP(S) capability
|
||||
URIs, registers every accepted URI with the category limiter, and starts
|
||||
`EventQueueGet` when advertised. Seed requests retry transient failures with
|
||||
bounded exponential backoff, stop permanently on HTTP 404, and are cancelled
|
||||
and joined when a simulator changes seed capability or disconnects.
|
||||
|
||||
The event queue sends the reference `{ ack, done }` LLSD/XML shape, begins with
|
||||
acknowledgement zero, and advances the acknowledgement only after a complete,
|
||||
valid `{ id, events }` response. Responses are limited to 1,024 events, 100,000
|
||||
nodes per body, and 8 MiB of binary data per body. Raw events are invoked in
|
||||
wire order, including unknown names. Known messages use the native Linden LLSD
|
||||
codecs and the same named/default CAPS callback registry as UDP; otherwise the
|
||||
generic packet catalog translates the body and feeds the bounded incoming UDP
|
||||
dispatch queue. Transient HTTP failures use bounded deterministic-jitter
|
||||
backoff, 404/410/499 close the poller, and graceful shutdown posts one final
|
||||
`done = true` acknowledgement. Cancellation interrupts an outstanding poll and
|
||||
joins its owned thread promptly.
|
||||
|
||||
## HTTP policy
|
||||
|
||||
Only absolute `http` and `https` request URIs are accepted. Response headers
|
||||
@@ -76,6 +97,9 @@ Run the deterministic issue gate with:
|
||||
|
||||
```sh
|
||||
cargo test -p libremetaverse --test caps_http
|
||||
cargo test -p libremetaverse --lib event_queue::tests
|
||||
cargo test -p libremetaverse --lib caps::tests
|
||||
cargo test -p libremetaverse --lib message_decoder::tests
|
||||
cargo test -p libremetaverse-compat-tests --test network_semantics queue_download
|
||||
cargo test -p libremetaverse-compat-tests --test network_semantics put_response_with_non_http_location
|
||||
```
|
||||
|
||||
@@ -38,15 +38,23 @@ TARGETS = {
|
||||
# implementations. The generated module keeps catalog markers and re-exports
|
||||
# the hand-written type so coverage remains deterministic.
|
||||
NATIVE_TYPES = {
|
||||
"T:LibreMetaverse.AgentDisplayName": "crate::message_decoder::AgentDisplayName",
|
||||
"T:LibreMetaverse.CapabilitiesReceivedEventArgs": "crate::caps::CapabilitiesReceivedEventArgs",
|
||||
"T:LibreMetaverse.Caps": "crate::caps::Caps",
|
||||
"T:LibreMetaverse.CapsRateLimiter": "crate::caps_http::CapsRateLimiter",
|
||||
"T:LibreMetaverse.CapsRateLimiterOptions": "crate::caps_http::CapsRateLimiterOptions",
|
||||
"T:LibreMetaverse.Http.DownloadManager": "crate::download_manager::DownloadManager",
|
||||
"T:LibreMetaverse.Http.DownloadRequest": "crate::download_manager::DownloadRequest",
|
||||
"T:LibreMetaverse.Http.EventQueueClient": "crate::event_queue::EventQueueClient",
|
||||
"T:LibreMetaverse.Http.EventQueueClient.ConnectedCallback": "crate::event_queue::EventQueueClientConnectedCallback",
|
||||
"T:LibreMetaverse.Http.EventQueueClient.EventCallback": "crate::event_queue::EventQueueClientEventCallback",
|
||||
"T:LibreMetaverse.HttpCapsClient": "crate::caps_http::HttpCapsClient",
|
||||
"T:LibreMetaverse.HttpCapsClient.ProgressReport": "crate::caps_http::HttpCapsClientProgressReport",
|
||||
"T:LibreMetaverse.MediaEntry": "crate::message_decoder::MediaEntry",
|
||||
"T:LibreMetaverse.Caps.EventQueueCallback": "crate::network_manager::CapsEventQueueCallback",
|
||||
"T:LibreMetaverse.CapsEventDictionary": "crate::network_manager::CapsEventDictionary",
|
||||
"T:LibreMetaverse.DisconnectedEventArgs": "crate::network_manager::DisconnectedEventArgs",
|
||||
"T:LibreMetaverse.EventQueueRunningEventArgs": "crate::network_manager::EventQueueRunningEventArgs",
|
||||
"T:LibreMetaverse.GenericStreamingMessageEventArgs": "crate::network_manager::GenericStreamingMessageEventArgs",
|
||||
"T:LibreMetaverse.NetworkManager.IncomingPacket": "crate::network_manager::NetworkManagerIncomingPacket",
|
||||
"T:LibreMetaverse.NetworkManager.OutgoingPacket": "crate::network_manager::NetworkManagerOutgoingPacket",
|
||||
@@ -57,6 +65,13 @@ NATIVE_TYPES = {
|
||||
"T:LibreMetaverse.SimConnectedEventArgs": "crate::network_manager::SimConnectedEventArgs",
|
||||
"T:LibreMetaverse.SimConnectingEventArgs": "crate::network_manager::SimConnectingEventArgs",
|
||||
"T:LibreMetaverse.SimDisconnectedEventArgs": "crate::network_manager::SimDisconnectedEventArgs",
|
||||
"T:LibreMetaverse.Messages.Linden.EventMessageBlock": "crate::event_queue::EventMessageBlock",
|
||||
"T:LibreMetaverse.Messages.Linden.EventQueueAck": "crate::event_queue::EventQueueAck",
|
||||
"T:LibreMetaverse.Messages.Linden.EventQueueEvent": "crate::event_queue::EventQueueEvent",
|
||||
"T:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent": "crate::event_queue::EventQueueEventQueueEvent",
|
||||
"T:LibreMetaverse.Messages.Linden.EventQueueGetMessage": "crate::event_queue::EventQueueGetMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage": "crate::message_decoder::AttachmentResourcesMessage",
|
||||
"T:LibreMetaverse.Primitive.PhysicsProperties": "crate::message_decoder::PrimitivePhysicsProperties",
|
||||
"T:LibreMetaverse.ArchetypeParam": "crate::genepool_catalog::ArchetypeParam",
|
||||
"T:LibreMetaverse.AttentionData": "crate::attention_catalog::AttentionData",
|
||||
"T:LibreMetaverse.AttentionSet": "crate::attention_catalog::AttentionSet",
|
||||
@@ -194,6 +209,8 @@ NATIVE_MEMBER_BODIES = {
|
||||
"self.native_set_http_caps_client(value)",
|
||||
"E:LibreMetaverse.NetworkManager.Disconnected":
|
||||
"self.native_subscribe_disconnected(handler)",
|
||||
"E:LibreMetaverse.NetworkManager.EventQueueRunning":
|
||||
"self.native_subscribe_event_queue_running(handler)",
|
||||
"E:LibreMetaverse.NetworkManager.GenericStreamingMessage":
|
||||
"self.native_subscribe_generic_streaming_message(handler)",
|
||||
"E:LibreMetaverse.NetworkManager.PacketSent":
|
||||
@@ -228,6 +245,20 @@ NATIVE_MEMBER_BODIES = {
|
||||
"self.native_enqueue_outgoing(packet)",
|
||||
"M:LibreMetaverse.NetworkManager.FindSimulator(System.Net.IPEndPoint)":
|
||||
"Ok(self.native_find_endpoint(end_point))",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.DecodeEvent(System.String,LibreMetaverse.StructuredData.OSDMap)":
|
||||
"crate::message_decoder::decode_event(&event_name, &map)",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.FromDictionaryString(System.Collections.Generic.Dictionary{System.String,System.String})":
|
||||
"crate::message_decoder::from_dictionary_string(dict)",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.FromDictionaryUri(System.Collections.Generic.Dictionary{System.Uri,System.Uri})":
|
||||
"crate::message_decoder::from_dictionary_uri(dict)",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.FromIP(System.Net.IPAddress)":
|
||||
"crate::message_decoder::from_ip(address)",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.ToDictionaryString(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::message_decoder::to_dictionary_string(osd)",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.ToDictionaryUri(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::message_decoder::to_dictionary_uri(osd)",
|
||||
"M:LibreMetaverse.Messages.MessageUtils.ToIP(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::message_decoder::to_ip(osd)",
|
||||
"M:LibreMetaverse.NetworkManager.FindSimulator(System.UInt64)":
|
||||
"Ok(self.native_find_handle(handle))",
|
||||
"M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})":
|
||||
@@ -280,6 +311,8 @@ NATIVE_MEMBER_BODIES = {
|
||||
"self.native_equals(obj)",
|
||||
"M:LibreMetaverse.Simulator.GetHashCode":
|
||||
"self.native_hash_code()",
|
||||
"M:LibreMetaverse.Simulator.IsEventQueueRunning(System.Boolean)":
|
||||
"self.native_is_event_queue_running(block_until_running)",
|
||||
"M:LibreMetaverse.Simulator.Pause":
|
||||
"self.native_pause()",
|
||||
"M:LibreMetaverse.Simulator.Resume":
|
||||
@@ -580,9 +613,15 @@ COMPOSED_BASE_TYPES = {
|
||||
}
|
||||
|
||||
NATIVE_MESSAGE_CODEC_TYPES = {
|
||||
"T:LibreMetaverse.Messages.Linden.AgentDropGroupMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.AgentData",
|
||||
"T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.GroupData",
|
||||
"T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.NewGroupData",
|
||||
"T:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.FolderDataInfo",
|
||||
"T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.ItemDataInfo",
|
||||
"T:LibreMetaverse.Messages.Linden.ChatSessionAcceptInvitation",
|
||||
"T:LibreMetaverse.Messages.Linden.ChatSessionRequestBlock",
|
||||
"T:LibreMetaverse.Messages.Linden.ChatSessionRequestMuteUpdate",
|
||||
@@ -593,16 +632,29 @@ NATIVE_MESSAGE_CODEC_TYPES = {
|
||||
"T:LibreMetaverse.Messages.Linden.ChatterBoxSessionStartReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ChatterboxSessionEventReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.CopyInventoryFromNotecardMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.CrossedRegionMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.DirLandReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.DirLandReplyMessage.QueryReply",
|
||||
"T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage.SimulatorInfoBlock",
|
||||
"T:LibreMetaverse.Messages.Linden.EstablishAgentCommunicationMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.GetObjectCostMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.GetObjectCostRequest",
|
||||
"T:LibreMetaverse.Messages.Linden.LandResourcesMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.LandResourcesRequest",
|
||||
"T:LibreMetaverse.Messages.Linden.LandStatReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.LandStatReplyMessage.ReportDataBlock",
|
||||
"T:LibreMetaverse.Messages.Linden.MapLayerMessageBase",
|
||||
"T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant",
|
||||
"T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant.LayerData",
|
||||
"T:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ObjectMediaRequest",
|
||||
"T:LibreMetaverse.Messages.Linden.ObjectMediaResponse",
|
||||
"T:LibreMetaverse.Messages.Linden.ObjectMediaUpdate",
|
||||
"T:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage.PrimOwner",
|
||||
"T:LibreMetaverse.Messages.Linden.ParcelPropertiesMessage",
|
||||
@@ -612,9 +664,17 @@ NATIVE_MESSAGE_CODEC_TYPES = {
|
||||
"T:LibreMetaverse.Messages.Linden.PlacesReplyMessage.QueryData",
|
||||
"T:LibreMetaverse.Messages.Linden.ProvisionVoiceAccountRequestMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.RemoteParcelRequestReply",
|
||||
"T:LibreMetaverse.Messages.Linden.RegionInfoMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.RenderMaterialsMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.RequiredVoiceVersionMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.ScriptRunningReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.SearchStatRequestReply",
|
||||
"T:LibreMetaverse.Messages.Linden.SearchStatRequestRequest",
|
||||
"T:LibreMetaverse.Messages.Linden.SendPostcardMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.SetDisplayNameMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.TeleportFailedMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.TeleportFinishMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.UpdateAgentInformationMessage",
|
||||
@@ -622,6 +682,8 @@ NATIVE_MESSAGE_CODEC_TYPES = {
|
||||
"T:LibreMetaverse.Messages.Linden.UpdateAgentLanguageMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.UpdateScriptAgentRequestMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.UpdateScriptTaskUpdateMessage",
|
||||
"T:LibreMetaverse.Messages.Linden.UploaderRequestComplete",
|
||||
"T:LibreMetaverse.Messages.Linden.UploaderRequestUpload",
|
||||
"T:LibreMetaverse.Messages.Linden.ViewerStatsMessage",
|
||||
}
|
||||
OPEN_ENUM_IDS = {"T:LibreMetaverse.BakeType", "T:LibreMetaverse.HoleType"}
|
||||
@@ -1341,7 +1403,22 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
|
||||
continue
|
||||
mapped_arguments = [mapper.type(argument, None, set(names)) for argument in arguments]
|
||||
trait_path = mapper.resolved[base] + generic_suffix(mapped_arguments)
|
||||
lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}")
|
||||
if (
|
||||
base == "LibreMetaverse.Interfaces.IMessage"
|
||||
and item["doc_id"] in NATIVE_MESSAGE_CODEC_TYPES
|
||||
):
|
||||
lines += [
|
||||
f"impl{suffix} {trait_path} for {rust_name}{suffix} {{",
|
||||
" fn deserialize(&mut self, map: libremetaverse_structured_data::OSDMap) -> Result<(), crate::Error> {",
|
||||
" <Self as crate::message_codec::GeneratedMessage>::deserialize_generated(self, &map)",
|
||||
" }",
|
||||
" fn serialize(&self) -> Result<libremetaverse_structured_data::OSDMap, crate::Error> {",
|
||||
" <Self as crate::message_codec::GeneratedMessage>::serialize_generated(self)",
|
||||
" }",
|
||||
"}",
|
||||
]
|
||||
else:
|
||||
lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user