Implement remaining TestClient service commands (#94)
This commit is contained in:
17
README.md
17
README.md
@@ -501,13 +501,16 @@ link-aware hierarchy traversal, and deterministic export. Their command
|
||||
surfaces, limits, isolated CLI tests, and the status of every remaining program
|
||||
are documented in the [`native programs guide`](programs/README.md).
|
||||
|
||||
The native `TestClient` now exposes its system, communication, inventory,
|
||||
appearance, asset, movement, object, parcel, estate, and grid command groups
|
||||
through one bounded multi-client shell. World commands use the native manager
|
||||
The native `TestClient` now exposes every non-voice system, communication,
|
||||
inventory, appearance, asset, movement, object, parcel, estate, grid, social,
|
||||
directory, and statistics command group through one bounded multi-client shell.
|
||||
World commands use the native manager
|
||||
APIs for movement and teleportation, object inspection and linkset transfer,
|
||||
parcel and map queries, estate terrain transfer, texture caching, and primitive
|
||||
creation. Offline fake-world directives exercise the same parsing, filtering,
|
||||
output, cancellation, and safety paths without connecting to a privileged live
|
||||
grid. Command ownership, resource limits, explicit live-operation gates, and
|
||||
the focused verification command are documented in the
|
||||
creation. Social and search commands use cancellable friend, group, avatar, and
|
||||
paged-directory APIs; complete profile cloning combines capability data with
|
||||
the simulator's interest block. Offline fixtures exercise the same parsing,
|
||||
filtering, output, cancellation, and safety paths without connecting to a
|
||||
privileged live grid. Command ownership, resource limits, explicit
|
||||
live-operation gates, and the focused verification command are documented in the
|
||||
[`TestClient` guide](docs/test-client.md).
|
||||
|
||||
@@ -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: 401 types / 17,026 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 401 types / 17,027 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 | native implementation: 164 types / 768 members; no generated shims remain |
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! Bounded decoder for the simulator's binary BVH animation asset format.
|
||||
|
||||
#![allow(clippy::needless_pass_by_value, clippy::unused_self)] // Mapped C# signatures are fixed.
|
||||
#![allow(
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::too_many_lines,
|
||||
clippy::unused_self
|
||||
)] // Mapped C# signatures and the source catalog are fixed.
|
||||
|
||||
use libremetaverse_types::compat::Object;
|
||||
use libremetaverse_types::{Error, UUID, Vector3};
|
||||
@@ -16,6 +20,163 @@ pub(crate) fn dance1() -> UUID {
|
||||
.expect("built-in DANCE1 UUID must be valid")
|
||||
}
|
||||
|
||||
/// Returns the complete built-in viewer animation catalog from the pinned
|
||||
/// `LibreMetaverse` source snapshot.
|
||||
pub(crate) fn builtin_animations() -> std::collections::HashMap<UUID, String> {
|
||||
const ENTRIES: &[(&str, &str)] = &[
|
||||
("AFRAID", "6b61c8e8-4747-0d75-12d7-e49ff207a4ca"),
|
||||
("AIM_BAZOOKA_R", "b5b4a67d-0aee-30d2-72cd-77b333e932ef"),
|
||||
("AIM_BOW_L", "46bb4359-de38-4ed8-6a22-f1f52fe8f506"),
|
||||
("AIM_HANDGUN_R", "3147d815-6338-b932-f011-16b56d9ac18b"),
|
||||
("AIM_RIFLE_R", "ea633413-8006-180a-c3ba-96dd1d756720"),
|
||||
("ANGRY", "5747a48e-073e-c331-f6f3-7c2149613d3e"),
|
||||
("AWAY", "fd037134-85d4-f241-72c6-4f42164fedee"),
|
||||
("BACKFLIP", "c4ca6188-9127-4f31-0158-23c4e2f93304"),
|
||||
("BELLY_LAUGH", "18b3a4b5-b463-bd48-e4b6-71eaac76c515"),
|
||||
("BLOW_KISS", "db84829b-462c-ee83-1e27-9bbee66bd624"),
|
||||
("BORED", "b906c4ba-703b-1940-32a3-0c7f7d791510"),
|
||||
("BOW", "82e99230-c906-1403-4d9c-3889dd98daba"),
|
||||
("BRUSH", "349a3801-54f9-bf2c-3bd0-1ac89772af01"),
|
||||
("BUSY", "efcf670c-2d18-8128-973a-034ebc806b67"),
|
||||
("CLAP", "9b0c1c4e-8ac7-7969-1494-28c874c4f668"),
|
||||
("COURTBOW", "9ba1c942-08be-e43a-fb29-16ad440efc50"),
|
||||
("CROUCH", "201f3fdf-cb1f-dbec-201f-7333e328ae7c"),
|
||||
("CROUCHWALK", "47f5f6fb-22e5-ae44-f871-73aaaf4a6022"),
|
||||
("CRY", "92624d3e-1068-f1aa-a5ec-8244585193ed"),
|
||||
("CUSTOMIZE", "038fcec9-5ebd-8a8e-0e2e-6e71a0a1ac53"),
|
||||
("CUSTOMIZE_DONE", "6883a61a-b27b-5914-a61e-dda118a9ee2c"),
|
||||
("DANCE1", "b68a3d7c-de9e-fc87-eec8-543d787e5b0d"),
|
||||
("DANCE2", "928cae18-e31d-76fd-9cc9-2f55160ff818"),
|
||||
("DANCE3", "30047778-10ea-1af7-6881-4db7a3a5a114"),
|
||||
("DANCE4", "951469f4-c7b2-c818-9dee-ad7eea8c30b7"),
|
||||
("DANCE5", "4bd69a1d-1114-a0b4-625f-84e0a5237155"),
|
||||
("DANCE6", "cd28b69b-9c95-bb78-3f94-8d605ff1bb12"),
|
||||
("DANCE7", "a54d8ee2-28bb-80a9-7f0c-7afbbe24a5d6"),
|
||||
("DANCE8", "b0dc417c-1f11-af36-2e80-7e7489fa7cdc"),
|
||||
("DEAD", "57abaae6-1d17-7b1b-5f98-6d11a6411276"),
|
||||
("DRINK", "0f86e355-dd31-a61c-fdb0-3a96b9aad05f"),
|
||||
("EMBARRASSED", "514af488-9051-044a-b3fc-d4dbf76377c6"),
|
||||
("EXPRESS_AFRAID", "aa2df84d-cf8f-7218-527b-424a52de766e"),
|
||||
("EXPRESS_ANGER", "1a03b575-9634-b62a-5767-3a679e81f4de"),
|
||||
("EXPRESS_BORED", "214aa6c1-ba6a-4578-f27c-ce7688f61d0d"),
|
||||
("EXPRESS_CRY", "d535471b-85bf-3b4d-a542-93bea4f59d33"),
|
||||
("EXPRESS_DISDAIN", "d4416ff1-09d3-300f-4183-1b68a19b9fc1"),
|
||||
(
|
||||
"EXPRESS_EMBARRASSED",
|
||||
"0b8c8211-d78c-33e8-fa28-c51a9594e424",
|
||||
),
|
||||
("EXPRESS_FROWN", "fee3df48-fa3d-1015-1e26-a205810e3001"),
|
||||
("EXPRESS_KISS", "1e8d90cc-a84e-e135-884c-7c82c8b03a14"),
|
||||
("EXPRESS_LAUGH", "62570842-0950-96f8-341c-809e65110823"),
|
||||
("EXPRESS_OPEN_MOUTH", "d63bc1f9-fc81-9625-a0c6-007176d82eb7"),
|
||||
("EXPRESS_REPULSED", "f76cda94-41d4-a229-2872-e0296e58afe1"),
|
||||
("EXPRESS_SAD", "eb6ebfb2-a4b3-a19c-d388-4dd5c03823f7"),
|
||||
("EXPRESS_SHRUG", "a351b1bc-cc94-aac2-7bea-a7e6ebad15ef"),
|
||||
("EXPRESS_SMILE", "b7c7c833-e3d3-c4e3-9fc0-131237446312"),
|
||||
("EXPRESS_SURPRISE", "728646d9-cc79-08b2-32d6-937f0a835c24"),
|
||||
("EXPRESS_TONGUE_OUT", "835965c6-7f2f-bda2-5deb-2478737f91bf"),
|
||||
("EXPRESS_TOOTHSMILE", "b92ec1a5-e7ce-a76b-2b05-bcdb9311417e"),
|
||||
("EXPRESS_WINK", "da020525-4d94-59d6-23d7-81fdebf33148"),
|
||||
("EXPRESS_WORRY", "9c05e5c7-6f07-6ca4-ed5a-b230390c3950"),
|
||||
("FALLDOWN", "666307d9-a860-572d-6fd4-c3ab8865c094"),
|
||||
("FEMALE_WALK", "f5fc7433-043d-e819-8298-f519a119b688"),
|
||||
("FINGER_WAG", "c1bc7f36-3ba0-d844-f93c-93be945d644f"),
|
||||
("FIST_PUMP", "7db00ccd-f380-f3ee-439d-61968ec69c8a"),
|
||||
("FLY", "aec4610c-757f-bc4e-c092-c6e9caf18daf"),
|
||||
("FLYSLOW", "2b5a38b2-5e00-3a97-a495-4c826bc443e6"),
|
||||
("HELLO", "9b29cd61-c45b-5689-ded2-91756b8d76a9"),
|
||||
("HOLD_BAZOOKA_R", "ef62d355-c815-4816-2474-b1acc21094a6"),
|
||||
("HOLD_BOW_L", "8b102617-bcba-037b-86c1-b76219f90c88"),
|
||||
("HOLD_HANDGUN_R", "efdc1727-8b8a-c800-4077-975fc27ee2f2"),
|
||||
("HOLD_RIFLE_R", "3d94bad0-c55b-7dcc-8763-033c59405d33"),
|
||||
("HOLD_THROW_R", "7570c7b5-1f22-56dd-56ef-a9168241bbb6"),
|
||||
("HOVER", "4ae8016b-31b9-03bb-c401-b1ea941db41d"),
|
||||
("HOVER_DOWN", "20f063ea-8306-2562-0b07-5c853b37b31e"),
|
||||
("HOVER_UP", "62c5de58-cb33-5743-3d07-9e4cd4352864"),
|
||||
("IMPATIENT", "5ea3991f-c293-392e-6860-91dfa01278a3"),
|
||||
("JUMP", "2305bd75-1ca9-b03b-1faa-b176b8a8c49e"),
|
||||
("JUMP_FOR_JOY", "709ea28e-1573-c023-8bf8-520c8bc637fa"),
|
||||
("KISS_MY_BUTT", "19999406-3a3a-d58c-a2ac-d72e555dcf51"),
|
||||
("LAND", "7a17b059-12b2-41b1-570a-186368b6aa6f"),
|
||||
("LAUGH_SHORT", "ca5b3f14-3194-7a2b-c894-aa699b718d1f"),
|
||||
("MEDIUM_LAND", "f4f00d6e-b9fe-9292-f4cb-0ae06ea58d57"),
|
||||
("MOTORCYCLE_SIT", "08464f78-3a8e-2944-cba5-0c94aff3af29"),
|
||||
("MUSCLE_BEACH", "315c3a41-a5f3-0ba4-27da-f893f769e69b"),
|
||||
("NO", "5a977ed9-7f72-44e9-4c4c-6e913df8ae74"),
|
||||
("NO_UNHAPPY", "d83fa0e5-97ed-7eb2-e798-7bd006215cb4"),
|
||||
("NYAH_NYAH", "f061723d-0a18-754f-66ee-29a44795a32f"),
|
||||
("ONETWO_PUNCH", "eefc79be-daae-a239-8c04-890f5d23654a"),
|
||||
("PEACE", "b312b10e-65ab-a0a4-8b3c-1326ea8e3ed9"),
|
||||
("POINT_ME", "17c024cc-eef2-f6a0-3527-9869876d7752"),
|
||||
("POINT_YOU", "ec952cca-61ef-aa3b-2789-4d1344f016de"),
|
||||
("PRE_JUMP", "7a4e87fe-de39-6fcb-6223-024b00893244"),
|
||||
("PUNCH_LEFT", "f3300ad9-3462-1d07-2044-0fef80062da0"),
|
||||
("PUNCH_RIGHT", "c8e42d32-7310-6906-c903-cab5d4a34656"),
|
||||
("REPULSED", "36f81a92-f076-5893-dc4b-7c3795e487cf"),
|
||||
("ROUNDHOUSE_KICK", "49aea43b-5ac3-8a44-b595-96100af0beda"),
|
||||
("RPS_COUNTDOWN", "35db4f7e-28c2-6679-cea9-3ee108f7fc7f"),
|
||||
("RPS_PAPER", "0836b67f-7f7b-f37b-c00a-460dc1521f5a"),
|
||||
("RPS_ROCK", "42dd95d5-0bc6-6392-f650-777304946c0f"),
|
||||
("RPS_SCISSORS", "16803a9f-5140-e042-4d7b-d28ba247c325"),
|
||||
("RUN", "05ddbff8-aaa9-92a1-2b74-8fe77a29b445"),
|
||||
("SAD", "0eb702e2-cc5a-9a88-56a5-661a55c0676a"),
|
||||
("SALUTE", "cd7668a6-7011-d7e2-ead8-fc69eff1a104"),
|
||||
("SHOOT_BOW_L", "e04d450d-fdb5-0432-fd68-818aaf5935f8"),
|
||||
("SHOUT", "6bd01860-4ebd-127a-bb3d-d1427e8e0c42"),
|
||||
("SHRUG", "70ea714f-3a97-d742-1b01-590a8fcd1db5"),
|
||||
("SIT", "1a5fe8ac-a804-8a5d-7cbd-56bd83184568"),
|
||||
("SIT_FEMALE", "b1709c8d-ecd3-54a1-4f28-d55ac0840782"),
|
||||
("SIT_GENERIC", "245f3c54-f1c0-bf2e-811f-46d8eeb386e7"),
|
||||
("SIT_GROUND", "1c7600d6-661f-b87b-efe2-d7421eb93c86"),
|
||||
(
|
||||
"SIT_GROUND_staticRAINED",
|
||||
"1a2bd58e-87ff-0df8-0b4c-53e047b0bb6e",
|
||||
),
|
||||
("SIT_TO_STAND", "a8dee56f-2eae-9e7a-05a2-6fb92b97e21e"),
|
||||
("SLEEP", "f2bed5f9-9d44-39af-b0cd-257b2a17fe40"),
|
||||
("SMOKE_IDLE", "d2f2ee58-8ad1-06c9-d8d3-3827ba31567a"),
|
||||
("SMOKE_INHALE", "6802d553-49da-0778-9f85-1599a2266526"),
|
||||
("SMOKE_THROW_DOWN", "0a9fb970-8b44-9114-d3a9-bf69cfe804d6"),
|
||||
("SNAPSHOT", "eae8905b-271a-99e2-4c0e-31106afd100c"),
|
||||
("STAND", "2408fe9e-df1d-1d7d-f4ff-1384fa7b350f"),
|
||||
("STANDUP", "3da1d753-028a-5446-24f3-9c9b856d9422"),
|
||||
("STAND_1", "15468e00-3400-bb66-cecc-646d7c14458e"),
|
||||
("STAND_2", "370f3a20-6ca6-9971-848c-9a01bc42ae3c"),
|
||||
("STAND_3", "42b46214-4b44-79ae-deb8-0df61424ff4b"),
|
||||
("STAND_4", "f22fed8b-a5ed-2c93-64d5-bdd8b93c889f"),
|
||||
("STRETCH", "80700431-74ec-a008-14f8-77575e73693f"),
|
||||
("STRIDE", "1cb562b0-ba21-2202-efb3-30f82cdf9595"),
|
||||
("SURF", "41426836-7437-7e89-025d-0aa4d10f1d69"),
|
||||
("SURPRISE", "313b9881-4302-73c0-c7d0-0e7a36b6c224"),
|
||||
("SWORD_STRIKE", "85428680-6bf9-3e64-b489-6f81087c24bd"),
|
||||
("TALK", "5c682a95-6da4-a463-0bf6-0f5b7be129d1"),
|
||||
("TANTRUM", "11000694-3f41-adc2-606b-eee1d66f3724"),
|
||||
("THROW_R", "aa134404-7dac-7aca-2cba-435f9db875ca"),
|
||||
("TRYON_SHIRT", "83ff59fe-2346-f236-9009-4e3608af64c1"),
|
||||
("TURNLEFT", "56e0ba0d-4a9f-7f27-6117-32f2ebbf6135"),
|
||||
("TURNRIGHT", "2d6daa51-3192-6794-8e2e-a15f8338ec30"),
|
||||
("TYPE", "c541c47f-e0c0-058b-ad1a-d6ae3a4584d9"),
|
||||
("WALK", "6ed24bd8-91aa-4b12-ccc7-c97c857ab4e0"),
|
||||
("WHISPER", "7693f268-06c7-ea71-fa21-2b30d6533f8f"),
|
||||
("WHISTLE", "b1ed7982-c68e-a982-7561-52a88a5298c0"),
|
||||
("WINK", "869ecdad-a44b-671e-3266-56aef2e3ac2e"),
|
||||
("WINK_HOLLYWOOD", "c0c4030f-c02b-49de-24ba-2331f43fe41c"),
|
||||
("WORRY", "9f496bd2-589a-709f-16cc-69bf7df1d36c"),
|
||||
("YES", "15dd911d-be82-2856-26db-27659b142875"),
|
||||
("YES_HAPPY", "b8c8b2a3-9008-1771-3bfc-90924955ab2d"),
|
||||
("YOGA_FLOAT", "42ecd00b-9947-a97c-400a-bbc9174c7aeb"),
|
||||
];
|
||||
ENTRIES
|
||||
.iter()
|
||||
.map(|(name, id)| {
|
||||
(
|
||||
UUID::new_with_string((*id).to_owned()).expect("built-in animation UUID is valid"),
|
||||
(*name).to_owned(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BinBVHJointKey {
|
||||
pub key_element: Vector3,
|
||||
@@ -338,3 +499,18 @@ impl<'a> Cursor<'a> {
|
||||
String::from_utf8(bytes.to_vec()).map_err(|_| Error::Argument)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn built_in_catalog_contains_every_pinned_animation_once() {
|
||||
let catalog = builtin_animations();
|
||||
assert_eq!(catalog.len(), 135);
|
||||
assert_eq!(catalog.get(&dance1()).map(String::as_str), Some("DANCE1"));
|
||||
let yoga = UUID::new_with_string("42ecd00b-9947-a97c-400a-bbc9174c7aeb".into())
|
||||
.expect("YOGA_FLOAT UUID");
|
||||
assert_eq!(catalog.get(&yoga).map(String::as_str), Some("YOGA_FLOAT"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2811,7 +2811,9 @@ impl Animations {
|
||||
libremetaverse_types::compat::FrozenDictionary<libremetaverse_types::UUID, String>,
|
||||
crate::Error,
|
||||
> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.Animations.ToDictionary")
|
||||
Ok(libremetaverse_types::compat::FrozenDictionary(
|
||||
crate::animation::builtin_animations(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
.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()
|
||||
@@ -260,6 +265,15 @@ impl GroupManager {
|
||||
pub fn current_group_count(&self) -> i32 {
|
||||
self.inner.group_count.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Returns a stable snapshot of the groups currently reported for this agent.
|
||||
///
|
||||
/// The cache is replaced atomically whenever an `AgentGroupDataUpdate` arrives,
|
||||
/// so callers never observe a partially decoded group list.
|
||||
#[must_use]
|
||||
pub fn current_groups(&self) -> HashMap<UUID, Group> {
|
||||
read(&self.inner.groups).clone()
|
||||
}
|
||||
pub fn group_membership_limit(&self) -> i32 {
|
||||
let mut client = (*self.inner.client).clone();
|
||||
client.self_().benefits().group_membership_limit()
|
||||
|
||||
@@ -3,8 +3,26 @@
|
||||
The `test-client` binary is a native Rust multi-client shell. It does not load
|
||||
the former C# executable or start a CLR process. The command registry owns the
|
||||
implemented system, communication, inventory, appearance, asset, movement,
|
||||
object, parcel, estate, and grid commands, and reports the remaining upstream
|
||||
targets through `pending_test_client_commands()`.
|
||||
object, parcel, estate, grid, agent, friends, groups, directory, and statistics
|
||||
commands. `pending_test_client_commands()` now reports only the two explicitly
|
||||
owned voice-native adapters (`ParcelVoiceInfo` and `VoiceAcountCommand`).
|
||||
|
||||
## Social, directory, and statistics behavior
|
||||
|
||||
Directory searches use native correlated, paged result streams and retain the
|
||||
upstream 20-second response bound. Group cache, member, and role requests are
|
||||
correlated with their native events and preserve the upstream timeouts. Friend
|
||||
mapping, UUID/name resolution, event details, and group operations propagate
|
||||
shell cancellation.
|
||||
|
||||
`who` maintains a live coarse-location roster and resolves display names;
|
||||
`bots` also tracks source UUIDs from decoded viewer-effect packets. Profile
|
||||
cloning combines the AgentProfile capability with the matching legacy interests
|
||||
reply to replace profile text, images, URL, interests, and picks before joining
|
||||
and activating the target's public groups. `play list` exposes all 135 built-in
|
||||
animations from the pinned source catalog and also accepts arbitrary animation
|
||||
asset UUIDs. Statistics come from each simulator's thread-safe native counters.
|
||||
Directory and collection results are bounded at 65,535 entries.
|
||||
|
||||
## World command behavior
|
||||
|
||||
@@ -39,6 +57,13 @@ terrain download/upload additionally require `--allow-estate-actions`.
|
||||
Consequently the default invocation cannot move an avatar, alter or create an
|
||||
object, upload an asset, or invoke an estate-owner action.
|
||||
|
||||
Profile cloning, generic messages, animation starts, touches, group activation,
|
||||
invitations, joins, and leaves use the same two-part mutation gate. Read-only
|
||||
friend, group, directory, region, uptime, dilation, and statistics commands do
|
||||
not require a gate. Profile cloning and direct group joins additionally require
|
||||
`--allow-spending`, because an open group can charge an enrollment fee and the
|
||||
client cannot safely infer that fee before every UUID-based join.
|
||||
|
||||
Output paths reject parent-directory traversal. Timeouts, cancellation, file
|
||||
limits, result limits, and linkset limits apply equally to live and fake-grid
|
||||
runs. Passwords and login secrets continue to pass through the shell's common
|
||||
@@ -52,13 +77,22 @@ Fake scripts can seed the same command layer with `!world-region`,
|
||||
`!world-agent-location`, `!world-estate`, `!world-asset`, and `!world-syntax`
|
||||
tab-separated directives. These fixtures never open a network connection.
|
||||
|
||||
The service layer additionally accepts `!service-avatar`, `!service-friend`,
|
||||
`!service-group`, `!service-group-result`, `!service-group-member`,
|
||||
`!service-group-role`, `!service-person`, `!service-classified`,
|
||||
`!service-event`, `!service-event-info`, `!service-land`, `!service-place`,
|
||||
`!service-sim`, `!service-animation`, and `!service-utilization[-row]` fixtures.
|
||||
|
||||
Run the issue-focused compatibility gate with:
|
||||
|
||||
```text
|
||||
cargo test -p libremetaverse-programs --test test_client_world_cli
|
||||
cargo test -p libremetaverse-programs --test test_client_services_cli
|
||||
```
|
||||
|
||||
The gate drives every command owned by the movement/object/land/grid issue,
|
||||
checks the exact fake backend calls, verifies destructive-operation guards,
|
||||
round-trips an exported linkset through import, compares two exports byte for
|
||||
byte, and validates generated terrain, texture, and TGA files.
|
||||
The world gate drives every command owned by the movement/object/land/grid
|
||||
issue, checks the exact fake backend calls, round-trips a linkset through
|
||||
import, and validates generated assets. The services gate drives every one of
|
||||
the 28 remaining non-voice commands in a single offline session, verifies the
|
||||
destructive-operation guards, and proves the pending inventory contains only
|
||||
the two explicitly owned voice adapters.
|
||||
|
||||
@@ -13,7 +13,7 @@ here so a source entry is never mistaken for a completed port.
|
||||
| `prim-inspector` | PrimInspector | Implemented with live and deterministic fake-grid discovery |
|
||||
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
||||
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
||||
| `test-client` | TestClient | Native shell, registry, system, communication, inventory, appearance, asset, movement, object, parcel, estate, and grid groups implemented; remaining command group tracked by #94 |
|
||||
| `test-client` | TestClient | All non-voice command groups implemented with live and deterministic fake-grid backends; voice adapters tracked by #95 and #96 |
|
||||
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
||||
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
||||
|
||||
@@ -184,6 +184,13 @@ transfer, and grid map/layer/location/wind commands. `textures on` and
|
||||
primitive textures. Detailed command behavior and limits are in
|
||||
[`docs/test-client.md`](../docs/test-client.md).
|
||||
|
||||
The final non-voice wave adds nearby-agent and bot inspection, complete profile
|
||||
cloning, friend mapping, group membership and role operations, paged directory
|
||||
searches, event details, animation control, object touch, and region/network
|
||||
statistics. Live profile cloning combines the modern AgentProfile capability
|
||||
with the legacy interests reply so text, images, URL, interests, picks, and
|
||||
public groups are all synchronized.
|
||||
|
||||
The native `load` command reads a portable command-alias manifest instead of a
|
||||
CLR assembly. Each non-comment line is tab-separated
|
||||
`name<TAB>description<TAB>template`; `{args}` or `$*` inserts the quoted command
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Native `TestClient` shell, registry, and first-wave command groups.
|
||||
|
||||
mod inventory;
|
||||
mod services;
|
||||
mod world;
|
||||
|
||||
use crate::commands::TEST_CLIENT_COMMANDS;
|
||||
@@ -135,6 +136,34 @@ pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
|
||||
"ShowEffectsCommand",
|
||||
"SleepCommand",
|
||||
"WaitForLoginCommand",
|
||||
"BotsCommand",
|
||||
"CloneProfileCommand",
|
||||
"GenericMessageCommand",
|
||||
"PlayAnimationCommand",
|
||||
"TouchCommand",
|
||||
"WhoCommand",
|
||||
"Key2NameCommand",
|
||||
"SearchClassifiedsCommand",
|
||||
"SearchEventsCommand",
|
||||
"SearchGroupsCommand",
|
||||
"SearchLandCommand",
|
||||
"SearchPeopleCommand",
|
||||
"SearchPlacesCommand",
|
||||
"ShowEventDetailsCommand",
|
||||
"FriendsCommand",
|
||||
"MapFriendCommand",
|
||||
"ActivateGroupCommand",
|
||||
"GroupMembersCommand",
|
||||
"GroupRolesCommand",
|
||||
"GroupsCommand",
|
||||
"InviteGroupCommand",
|
||||
"JoinGroupCommand",
|
||||
"LeaveGroupCommand",
|
||||
"DilationCommand",
|
||||
"NetstatsCommand",
|
||||
"RegionInfoCommand",
|
||||
"StatsCommand",
|
||||
"UptimeCommand",
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
@@ -153,7 +182,7 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
|
||||
version,
|
||||
about = "Run the native LibreMetaverse multi-client command shell",
|
||||
long_about = None,
|
||||
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper, plus the inventory, appearance, asset, movement, object, parcel, estate, and grid groups. Run `help` or `help COMMAND` inside the shell for the complete command registry."
|
||||
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, and whisper. All non-voice inventory, appearance, asset, movement, object, parcel, estate, grid, social, directory, and statistics commands are also native. Run `help` or `help COMMAND` inside the shell for the complete command registry."
|
||||
)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Cli {
|
||||
@@ -296,7 +325,7 @@ enum ClientEvent {
|
||||
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
type InventoryFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
|
||||
|
||||
trait ClientBackend: Send + Sync + inventory::Backend + world::Backend {
|
||||
trait ClientBackend: Send + Sync + inventory::Backend + services::Backend + world::Backend {
|
||||
fn id(&self) -> UUID;
|
||||
fn name(&self) -> String;
|
||||
fn connected(&self) -> bool;
|
||||
@@ -332,6 +361,7 @@ struct LiveBackend {
|
||||
subscriptions: Mutex<Vec<Subscription>>,
|
||||
inventory_state: Mutex<inventory::State>,
|
||||
world_state: Mutex<world::State>,
|
||||
services_state: Mutex<services::State>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -393,6 +423,7 @@ impl LiveBackend {
|
||||
subscriptions: Mutex::new(Vec::new()),
|
||||
inventory_state: Mutex::new(inventory::State::new(account_policy)),
|
||||
world_state: Mutex::new(world::State::new(account_policy, get_textures)),
|
||||
services_state: Mutex::new(services::State::new(account_policy)),
|
||||
closed: AtomicBool::new(false),
|
||||
});
|
||||
backend.install(&events, &dropped);
|
||||
@@ -512,6 +543,7 @@ impl LiveBackend {
|
||||
},
|
||||
)));
|
||||
world::install_live(self, &mut subscriptions);
|
||||
services::install_live(self, &mut subscriptions);
|
||||
*lock(&self.subscriptions) = subscriptions;
|
||||
}
|
||||
}
|
||||
@@ -680,6 +712,7 @@ struct FakeBackend {
|
||||
group_members: Arc<Mutex<HashSet<UUID>>>,
|
||||
inventory_state: Mutex<inventory::State>,
|
||||
world_state: Mutex<world::State>,
|
||||
services_state: Mutex<services::State>,
|
||||
}
|
||||
|
||||
impl FakeBackend {
|
||||
@@ -700,6 +733,7 @@ impl FakeBackend {
|
||||
group_members,
|
||||
inventory_state: Mutex::new(inventory::State::new(policy)),
|
||||
world_state: Mutex::new(world::State::new(policy, get_textures)),
|
||||
services_state: Mutex::new(services::State::new(policy)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,10 +854,13 @@ impl ProgramOutput {
|
||||
enum CommandCategory {
|
||||
Appearance,
|
||||
Communication,
|
||||
Friends,
|
||||
Groups,
|
||||
Inventory,
|
||||
Movement,
|
||||
Objects,
|
||||
Parcel,
|
||||
Search,
|
||||
Simulator,
|
||||
TestClient,
|
||||
Other,
|
||||
@@ -838,6 +875,7 @@ enum CommandHandler {
|
||||
Im,
|
||||
ImGroup,
|
||||
Inventory(inventory::Command),
|
||||
Services(services::Command),
|
||||
World(world::Command),
|
||||
Load,
|
||||
Login,
|
||||
@@ -999,6 +1037,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
|
||||
.into_iter()
|
||||
.chain(communication_commands())
|
||||
.chain(inventory::commands())
|
||||
.chain(services::commands())
|
||||
.chain(world::commands())
|
||||
.map(|(name, description, category, handler)| {
|
||||
(
|
||||
@@ -1780,6 +1819,9 @@ async fn execute_client_command(
|
||||
CommandHandler::Inventory(command) => {
|
||||
inventory::execute(client.backend.as_ref(), *command, args, from, cancellation).await
|
||||
}
|
||||
CommandHandler::Services(command) => {
|
||||
services::execute(client.backend.as_ref(), *command, args, cancellation).await
|
||||
}
|
||||
CommandHandler::World(command) => {
|
||||
world::execute(client.backend.as_ref(), *command, args, cancellation).await
|
||||
}
|
||||
@@ -2367,6 +2409,10 @@ async fn run_fake_event_directive(
|
||||
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(result) = services::apply_fake_directive(manager, fields) {
|
||||
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
|
||||
return Ok(());
|
||||
}
|
||||
match fields {
|
||||
["chat", client, source, name, message] => {
|
||||
let event = ClientEvent::Chat {
|
||||
@@ -2670,6 +2716,7 @@ mod tests {
|
||||
assert!(TEST_CLIENT_COMMANDS.contains(command));
|
||||
assert!(!pending.contains(command));
|
||||
}
|
||||
assert_eq!(pending, ["ParcelVoiceInfo", "VoiceAcountCommand"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
2673
programs/src/test_client/services.rs
Normal file
2673
programs/src/test_client/services.rs
Normal file
File diff suppressed because it is too large
Load Diff
215
programs/tests/test_client_services_cli.rs
Normal file
215
programs/tests/test_client_services_cli.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
const CLIENT: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
const AVATAR: &str = "11111111-2222-3333-4444-555555555555";
|
||||
const BOT: &str = "22222222-3333-4444-5555-666666666666";
|
||||
const GROUP: &str = "33333333-4444-5555-6666-777777777777";
|
||||
const MEMBER: &str = "44444444-5555-6666-7777-888888888888";
|
||||
const ROLE: &str = "55555555-6666-7777-8888-999999999999";
|
||||
const CLASSIFIED: &str = "66666666-7777-8888-9999-aaaaaaaaaaaa";
|
||||
const LAND: &str = "77777777-8888-9999-aaaa-bbbbbbbbbbbb";
|
||||
const SIM: &str = "88888888-9999-aaaa-bbbb-cccccccccccc";
|
||||
const OBJECT: &str = "99999999-aaaa-bbbb-cccc-dddddddddddd";
|
||||
const DANCE1: &str = "b68a3d7c-de9e-fc87-eec8-543d787e5b0d";
|
||||
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestDir(PathBuf);
|
||||
impl TestDir {
|
||||
fn new() -> Self {
|
||||
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"metacrate-test-client-services-{}-{id}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&path).expect("create test directory");
|
||||
Self(path)
|
||||
}
|
||||
fn write(&self, contents: &str) -> PathBuf {
|
||||
let path = self.0.join("session.tsv");
|
||||
fs::write(&path, contents).expect("write script");
|
||||
path
|
||||
}
|
||||
}
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
fn path_text(path: &Path) -> &str {
|
||||
path.to_str().expect("UTF-8 test path")
|
||||
}
|
||||
fn run(args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_test-client"))
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("run test-client")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn full_offline_session_exercises_every_remaining_non_voice_command() {
|
||||
let directory = TestDir::new();
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\n\
|
||||
!service-avatar\t{CLIENT}\t{AVATAR}\tActive Resident\tBuilders\t128\t129\t25\t10\ttrue\n\
|
||||
!service-avatar\t{CLIENT}\t{BOT}\tQuiet Bot\tNone\t130\t131\t25\t11\tfalse\n\
|
||||
!service-friend\t{CLIENT}\t{AVATAR}\tActive Resident\ttrue\t4294967298000\t128\t129\t25\n\
|
||||
!service-group\t{CLIENT}\t{GROUP}\tBuilders Guild\n\
|
||||
!service-group-result\t{CLIENT}\t{GROUP}\tBuilders Guild\t42\n\
|
||||
!service-group-member\t{CLIENT}\t{GROUP}\t{MEMBER}\n\
|
||||
!service-group-role\t{CLIENT}\t{GROUP}\t{ROLE}\tOfficer\tArchitect\n\
|
||||
!service-person\t{CLIENT}\t{AVATAR}\tActive\tResident\ttrue\n\
|
||||
!service-classified\t{CLIENT}\t{CLASSIFIED}\tBuilder Services\t25\n\
|
||||
!service-event\t{CLIENT}\t7\tBuilding Class\tToday\n\
|
||||
!service-event-info\t{CLIENT}\t7\tBuilding Class\tTest Region\t1000\t2000\t25\n\
|
||||
!service-land\t{CLIENT}\t{LAND}\tBuilder Parcel\t1024\t500\n\
|
||||
!service-place\t{CLIENT}\tBuilder Plaza\tTest Region\t128\t128\t25\n\
|
||||
!service-sim\t{CLIENT}\tTest Region\t{SIM}\t4294967298000\t0.98\n\
|
||||
!service-utilization\t{CLIENT}\ttrue\n\
|
||||
!service-utilization-row\t{CLIENT}\tpacket\tAgentUpdate\t2\t3\t2048\t4096\n\
|
||||
!service-utilization-row\t{CLIENT}\tcapability\tEventQueueGet\t1\t4\t512\t8192\n\
|
||||
who\n\
|
||||
bots\n\
|
||||
friends\n\
|
||||
mapfriend {AVATAR}\n\
|
||||
key2name {AVATAR}\n\
|
||||
key2name {GROUP}\n\
|
||||
searchclassifieds Builder\n\
|
||||
searchevents Building\n\
|
||||
searchgroups Builders\n\
|
||||
searchland mainland 1000 512\n\
|
||||
searchpeople Active\n\
|
||||
searchplaces Builder\n\
|
||||
showevent 7\n\
|
||||
groups\n\
|
||||
groupmembers Builders Guild\n\
|
||||
grouproles Builders Guild\n\
|
||||
play list\n\
|
||||
play show\n\
|
||||
dilation\n\
|
||||
netstats\n\
|
||||
regioninfo\n\
|
||||
stats\n\
|
||||
uptime\n\
|
||||
cloneprofile {AVATAR} --confirm\n\
|
||||
sendgeneric test-method alpha beta --confirm\n\
|
||||
play DANCE1 --confirm\n\
|
||||
touch {OBJECT} --confirm\n\
|
||||
activategroup Builders Guild --confirm\n\
|
||||
invitegroup {AVATAR} {GROUP} {ROLE} --confirm\n\
|
||||
joingroup Builders Guild --confirm\n\
|
||||
leavegroup Builders Guild --confirm\n\
|
||||
quit\n"
|
||||
));
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--allow-spending",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
let stderr = String::from_utf8(output.stderr).expect("UTF-8 stderr");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout:\n{stdout}\nstderr:\n{stderr}"
|
||||
);
|
||||
assert!(stderr.is_empty(), "{stderr}");
|
||||
for expected in [
|
||||
"Active Resident (Group: Builders",
|
||||
"Quiet Bot (Group: None",
|
||||
"Is Probably a bot",
|
||||
"has 1 friends:",
|
||||
"Found Friend 11111111-2222-3333-4444-555555555555",
|
||||
"Avatar: Active Resident",
|
||||
"Group: Builders Guild",
|
||||
"returned 1 classified ads",
|
||||
"matched 1 Events",
|
||||
"matched 1 Groups",
|
||||
"1 results",
|
||||
"matched 1 People",
|
||||
"returned 1 results",
|
||||
"<redacted-url> Region/232/208/0",
|
||||
"got 1 groups",
|
||||
"MemberCount 1",
|
||||
"RoleCount 1",
|
||||
"AFRAID",
|
||||
DANCE1,
|
||||
"Dilation is 0.98",
|
||||
"Capabilities Totals",
|
||||
"Packet Totals",
|
||||
"UUID: 88888888-9999-aaaa-bbbb-cccccccccccc",
|
||||
"Packets in the queue:",
|
||||
"I am Alice Bot, Up Since:",
|
||||
"Synchronized our profile",
|
||||
"Sent generic message with method test-method",
|
||||
"Touched object",
|
||||
"Active group is now Builders Guild",
|
||||
"invited",
|
||||
"Joined the group Builders Guild",
|
||||
"has left the group Builders Guild",
|
||||
"CALL profile-clone",
|
||||
"CALL generic-message",
|
||||
"CALL animation-start",
|
||||
"CALL object-touch",
|
||||
"CALL group-activate",
|
||||
"CALL group-invite",
|
||||
"CALL group-join",
|
||||
"CALL group-leave",
|
||||
] {
|
||||
assert!(stdout.contains(expected), "missing {expected:?}:\n{stdout}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn social_mutations_require_startup_and_per_command_confirmation() {
|
||||
let directory = TestDir::new();
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\nplay {DANCE1} --confirm\nquit\n"
|
||||
));
|
||||
let output = run(&["--fake-script", path_text(&script)]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
assert!(output.status.success());
|
||||
assert!(stdout.contains("Live mutations are disabled"), "{stdout}");
|
||||
assert!(!stdout.contains("CALL animation-start"), "{stdout}");
|
||||
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\ntouch {OBJECT}\nquit\n"
|
||||
));
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
assert!(output.status.success());
|
||||
assert!(stdout.contains("append --confirm"), "{stdout}");
|
||||
assert!(!stdout.contains("CALL object-touch"), "{stdout}");
|
||||
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\njoingroup uuid {GROUP} --confirm\nquit\n"
|
||||
));
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
assert!(output.status.success());
|
||||
assert!(stdout.contains("restart with --allow-spending"), "{stdout}");
|
||||
assert!(!stdout.contains("CALL group-join"), "{stdout}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_inventory_leaves_only_explicit_voice_adapters() {
|
||||
assert_eq!(
|
||||
libremetaverse_programs::test_client::pending_test_client_commands(),
|
||||
["ParcelVoiceInfo", "VoiceAcountCommand"]
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use libremetaverse::animesh::AnimeshManager;
|
||||
use libremetaverse::appearance::CurrentOutfitFolder;
|
||||
use libremetaverse::{
|
||||
AgentManager, AppearanceManagerWearableData, AssetDownload, AssetManager, AssetUpload,
|
||||
ChatEventArgs, ChatType, InventoryException, InventoryItem, InventoryManager,
|
||||
AgentManager, Animations, AppearanceManagerWearableData, AssetDownload, AssetManager,
|
||||
AssetUpload, ChatEventArgs, ChatType, InventoryException, InventoryItem, InventoryManager,
|
||||
TeleportEventArgs,
|
||||
};
|
||||
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription};
|
||||
@@ -116,3 +116,16 @@ fn avatar_facing_constructors_preserve_native_defaults_and_catalog_errors() {
|
||||
"M:LibreMetaverse.InventoryException.#ctor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_in_animation_dictionary_matches_the_pinned_source_catalog() {
|
||||
let catalog = Animations::to_dictionary().expect("animation dictionary").0;
|
||||
assert_eq!(catalog.len(), 135);
|
||||
assert_eq!(
|
||||
catalog.get(&Animations::dance1()).map(String::as_str),
|
||||
Some("DANCE1")
|
||||
);
|
||||
let yoga = UUID::new_with_string("42ecd00b-9947-a97c-400a-bbc9174c7aeb".into())
|
||||
.expect("YOGA_FLOAT UUID");
|
||||
assert_eq!(catalog.get(&yoga).map(String::as_str), Some("YOGA_FLOAT"));
|
||||
}
|
||||
|
||||
@@ -624,6 +624,8 @@ NATIVE_GENERATED_TYPES = {
|
||||
NATIVE_MEMBER_BODIES = {
|
||||
"F:LibreMetaverse.Animations.DANCE1":
|
||||
"crate::animation::dance1()",
|
||||
"M:LibreMetaverse.Animations.ToDictionary":
|
||||
"Ok(libremetaverse_types::compat::FrozenDictionary(crate::animation::builtin_animations()))",
|
||||
"M:LibreMetaverse.RLV.RlvCommon.TryGetAttachmentPointFromItemName(System.String,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint}@)":
|
||||
"Self::native_try_get_attachment_point_from_item_name(item_name, attachment_point)",
|
||||
"M:LibreMetaverse.RLV.RlvRestriction.#ctor(LibreMetaverse.RLV.RlvRestrictionType,System.Guid,System.String,System.Collections.Generic.ICollection{System.Object})":
|
||||
|
||||
Reference in New Issue
Block a user