Implement remaining TestClient service commands (#94)
Some checks failed
Native code generation / deterministic (push) Successful in 18m19s
Imaging and meshing gate / native (push) Failing after 4m28s
Native Rust workspace compile / compile (push) Failing after 12m58s

This commit is contained in:
2026-08-11 13:10:27 +00:00
parent 374d7958f1
commit 21f85a1b58
13 changed files with 3300 additions and 24 deletions

View File

@@ -10,9 +10,10 @@
use crate::client_core::ClientWeakHandle;
use crate::packet_catalog::PacketType;
use crate::packets::{
AvatarAnimationPacket, AvatarPickerRequestPacket, AvatarPropertiesRequestPacket,
ClassifiedInfoRequestPacket, GenericMessagePacket, GenericMessagePacketParamListBlock,
TrackAgentPacket, UUIDNameRequestPacket, UUIDNameRequestPacketUUIDNameBlockBlock,
AvatarAnimationPacket, AvatarInterestsReplyPacket, AvatarPickerRequestPacket,
AvatarPropertiesRequestPacket, ClassifiedInfoRequestPacket, GenericMessagePacket,
GenericMessagePacketParamListBlock, TrackAgentPacket, UUIDNameRequestPacket,
UUIDNameRequestPacketUUIDNameBlockBlock,
};
use crate::{Error, GridClient};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
@@ -121,6 +122,27 @@ fn handle_avatar_animation(inner: &AvatarManagerInner, data: Vec<u8>) {
});
}
fn decode_avatar_interests(data: Vec<u8>, avatar_id: UUID) -> Option<crate::AvatarInterests> {
let mut offset = 0;
let packet = AvatarInterestsReplyPacket::new_with_bytes_int32(data, &mut offset).ok()?;
if packet.agent_data.avatar_id != avatar_id {
return None;
}
Some(crate::AvatarInterests {
languages_text: String::from_utf8_lossy(&packet.properties_data.languages_text)
.trim_end_matches('\0')
.to_owned(),
skills_mask: packet.properties_data.skills_mask,
skills_text: String::from_utf8_lossy(&packet.properties_data.skills_text)
.trim_end_matches('\0')
.to_owned(),
want_to_mask: packet.properties_data.want_to_mask,
want_to_text: String::from_utf8_lossy(&packet.properties_data.want_to_text)
.trim_end_matches('\0')
.to_owned(),
})
}
impl AvatarAnimationEventArgs {
pub(crate) fn native_new(avatar_id: UUID, animations: Vec<Animation>) -> Result<Self, Error> {
if animations.len() > 256 {
@@ -476,6 +498,53 @@ impl AvatarManager {
)
}
/// Requests the legacy profile-interest block and waits for the matching
/// simulator reply.
///
/// The modern `AgentProfile` capability includes groups and picks but does
/// not include the five interest fields. Consumers that need a complete
/// editable profile can pair this request with [`Self::request_agent_profile`].
///
/// # Errors
///
/// Returns an error if there is no active simulator, the request cannot be
/// encoded or sent, the matching reply channel closes, or cancellation is
/// requested.
pub async fn request_avatar_interests(
&self,
avatar_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<crate::AvatarInterests, Error> {
let network = self.live_network()?;
let token = cancellation_token.unwrap_or_default();
token.throw_if_cancellation_requested()?;
let (sender, receiver) = tokio::sync::oneshot::channel();
let sender = Arc::new(Mutex::new(Some(sender)));
let callback = Arc::clone(&sender);
let subscription = network.subscribe_raw_packet(Arc::new(move |event| {
if event.packet_type != PacketType::AvatarInterestsReply {
return;
}
let Some(interests) = decode_avatar_interests(event.data, avatar_id) else {
return;
};
if let Some(sender) = callback
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
let _ = sender.send(interests);
}
}));
self.native_request_avatar_properties(avatar_id)?;
let result = tokio::select! {
value = receiver => value.map_err(|_| Error::InvalidOperation),
() = token.cancelled() => Err(Error::Cancelled),
};
drop(subscription);
result
}
pub(crate) fn native_request_avatar_name_search(
&self,
name: String,
@@ -1018,4 +1087,25 @@ mod tests {
assert_eq!(*received.lock().expect("event lock"), [0]);
}
#[test]
fn avatar_interests_reply_is_correlated_and_decoded() {
let avatar_id = UUID::random().expect("avatar id");
let mut packet = AvatarInterestsReplyPacket::new_with_constructor().expect("packet");
packet.agent_data.avatar_id = avatar_id;
packet.properties_data.languages_text = b"Rust, LSL\0".to_vec();
packet.properties_data.skills_mask = 0x12;
packet.properties_data.skills_text = b"Protocol testing\0".to_vec();
packet.properties_data.want_to_mask = 0x34;
packet.properties_data.want_to_text = b"Build things\0".to_vec();
let bytes = packet.to_bytes_with_method().expect("packet bytes");
assert!(decode_avatar_interests(bytes.clone(), UUID::zero()).is_none());
let interests = decode_avatar_interests(bytes, avatar_id).expect("matching reply");
assert_eq!(interests.languages_text, "Rust, LSL");
assert_eq!(interests.skills_mask, 0x12);
assert_eq!(interests.skills_text, "Protocol testing");
assert_eq!(interests.want_to_mask, 0x34);
assert_eq!(interests.want_to_text, "Build things");
}
}