Complete milestone 09 world integration gate (#75)
Some checks failed
Native code generation / deterministic (push) Failing after 8m37s
Imaging and meshing gate / native (push) Successful in 5m24s
Native Rust workspace compile / compile (push) Failing after 6m15s

This commit is contained in:
2026-08-10 21:19:43 +00:00
parent 55424dbd7f
commit fdda05b53f
18 changed files with 767 additions and 82 deletions

View File

@@ -129,6 +129,7 @@ struct ClientRuntime {
interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>,
friends_manager: Mutex<Option<Arc<crate::friends_manager::FriendsManagerInner>>>,
group_manager: Mutex<Option<Arc<crate::group_manager::GroupManagerInner>>>,
marketplace_manager: Mutex<Option<crate::marketplace_runtime::MarketplaceManager>>,
appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
@@ -139,6 +140,12 @@ struct ClientRuntime {
shutdown_wait: Mutex<()>,
}
fn clear_cached<T>(slot: &Mutex<Option<T>>) {
slot.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
impl ClientRuntime {
fn new(
services: Vec<Arc<dyn ClientService>>,
@@ -174,6 +181,7 @@ impl ClientRuntime {
interest_list_manager: Mutex::new(None),
friends_manager: Mutex::new(None),
group_manager: Mutex::new(None),
marketplace_manager: Mutex::new(None),
appearance_manager: Mutex::new(None),
avatar_manager: Mutex::new(None),
animesh_manager: Mutex::new(None),
@@ -248,42 +256,25 @@ impl ClientRuntime {
}
}
services.clear();
self.agent_throttle_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.inventory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.object_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.grid_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.directory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.interest_list_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
clear_cached(&self.agent_throttle_sender);
clear_cached(&self.inventory_manager);
clear_cached(&self.inventory_ais_client);
clear_cached(&self.asset_manager);
clear_cached(&self.appearance_manager);
clear_cached(&self.avatar_manager);
clear_cached(&self.animesh_manager);
clear_cached(&self.object_manager);
clear_cached(&self.environment_manager);
clear_cached(&self.estate_tools);
clear_cached(&self.terrain_manager);
clear_cached(&self.sound_manager);
clear_cached(&self.parcel_manager);
clear_cached(&self.grid_manager);
clear_cached(&self.directory_manager);
clear_cached(&self.interest_list_manager);
clear_cached(&self.friends_manager);
clear_cached(&self.group_manager);
clear_cached(&self.marketplace_manager);
self.http_caps_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -1106,6 +1097,32 @@ impl GridClient {
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn native_marketplace(&self) -> crate::marketplace_runtime::MarketplaceManager {
let mut cached = self
.runtime
.marketplace_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(manager) = cached.as_ref() {
return manager.clone();
}
let manager = crate::marketplace_runtime::MarketplaceManager::native_new_client_owned(self);
*cached = Some(manager.clone());
manager
}
pub(crate) fn native_set_marketplace(
&self,
value: crate::marketplace_runtime::MarketplaceManager,
) {
value.native_bind_to_client(self);
*self
.runtime
.marketplace_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
pub(crate) fn native_set_caps_rate_limiter(
&mut self,
value: crate::caps_http::CapsRateLimiter,

View File

@@ -6730,11 +6730,13 @@ impl GridClient {
}
/// C# member: `P:LibreMetaverse.GridClient.Marketplace`.
pub fn marketplace(&self) -> libremetaverse::marketplace::MarketplaceManager {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.Marketplace")
/* native client-core implementation */
self.native_marketplace()
}
/// Setter for C# member: `P:LibreMetaverse.GridClient.Marketplace`.
pub fn set_marketplace(&mut self, value: libremetaverse::marketplace::MarketplaceManager) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.Marketplace")
/* native client-core implementation */
self.native_set_marketplace(value)
}
/// C# member: `P:LibreMetaverse.GridClient.Network`.
pub fn network(&self) -> libremetaverse::NetworkManager {
@@ -9930,9 +9932,14 @@ impl InventoryManager {
>,
>,
) -> Result<Option<Option<libremetaverse_types::UUID>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryManager.UploadThumbnailAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})",
self.native_upload_thumbnail(
inventory_id,
task_id,
j2c_image_data,
cancellation_token,
progress,
)
.await
}
/// C# member: `M:LibreMetaverse.InventoryManager.WaitForNextInventoryOfferAsync(System.Threading.CancellationToken)`.
pub async fn wait_for_next_inventory_offer(
@@ -17640,6 +17647,7 @@ pub mod assets {
pub const TERRAINS_PATH: &'static str = "terrains/";
/// C# member: `M:LibreMetaverse.Assets.ArchiveConstants.#ctor`.
pub fn new() -> Result<Self, crate::Error> {
/* native zero-sized constants namespace */
Ok(Self)
}
}

View File

@@ -11,6 +11,7 @@ use libremetaverse_types::compat::{CancellationToken, HttpResponse, Uri};
use libremetaverse_types::{AssetType, UUID};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
const MAX_FOLDER_DEPTH_REQUEST: i32 = 50;
const MAX_AIS_OBJECTS: usize = 100_000;
@@ -110,6 +111,31 @@ impl InventoryAISClient {
caps.capability_uri(name.to_owned())
}
fn wait_for_capability(
&self,
name: &str,
cancellation: &CancellationToken,
) -> Result<Option<Uri>, Error> {
let client = self.client()?;
let Some(simulator) = client.native_network()?.current_sim() else {
return Ok(None);
};
let Some(caps) = simulator.native_caps() else {
return Ok(None);
};
let deadline = Instant::now() + Duration::from_secs(2);
loop {
cancellation.throw_if_cancellation_requested()?;
if let Some(uri) = caps.capability_uri(name.to_owned())? {
return Ok(Some(uri));
}
if caps.seed_request_finished() || Instant::now() >= deadline {
return Ok(None);
}
std::thread::sleep(Duration::from_millis(1));
}
}
pub(crate) fn native_is_available(&self) -> bool {
self.capability(Self::INVENTORY_CAP_NAME)
.ok()
@@ -147,7 +173,7 @@ impl InventoryAISClient {
) -> Result<Option<(HttpResponse, Vec<u8>)>, Error> {
let token = Self::token(cancellation_token);
token.throw_if_cancellation_requested()?;
let Some(capability) = self.capability(cap_name)? else {
let Some(capability) = self.wait_for_capability(cap_name, &token)? else {
return Ok(None);
};
let bytes = match payload {
@@ -359,18 +385,27 @@ impl InventoryAISClient {
new_inventory: OSD,
cancellation_token: Option<CancellationToken>,
) -> Result<Vec<InventoryItem>, Error> {
let path = format!("category/{parent_uuid}?tid={}", Self::tid()?);
let (_, _, _, links) = self
.mutation(
let Some((response, bytes)) = self
.request(
Self::INVENTORY_CAP_NAME,
"POST",
path,
format!("category/{parent_uuid}"),
BTreeMap::new(),
Some(new_inventory),
false,
cancellation_token,
)
.await?;
.await?
else {
return Ok(Vec::new());
};
if !Self::status_ok(&response) {
return Err(Error::HttpRequest);
}
let Some(map) = Self::decode_map(bytes)? else {
return Ok(Vec::new());
};
let (folders, items, links) = self.parse_all_map(&map)?;
self.reconcile(&map, &folders, &items, &links)?;
Ok(links)
}

View File

@@ -784,9 +784,10 @@ impl InventoryManager {
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
let client = client.ok_or(Error::ArgumentNull)?;
let initial_store = Inventory::new_with_grid_client(Arc::clone(&client))?;
let inner = Arc::new(InventoryManagerInner {
client: client.native_weak_handle(),
store: RwLock::new(None),
store: RwLock::new(Some(initial_store)),
disposed: AtomicBool::new(false),
callback_position: AtomicU32::new(0),
request_position: AtomicU64::new(0),
@@ -1914,10 +1915,9 @@ impl InventoryManager {
.await?
.ok_or(Error::InvalidOperation)?;
let uri = Uri(format!(
"{}/category/{}?tid={}",
"{}/category/{}",
cap.0.trim_end_matches('/'),
folder_id,
UUID::random()?
folder_id
));
let payload = OSD::Map(HashMap::from([(
"links".to_owned(),
@@ -4397,6 +4397,103 @@ fn parse_task_inventory_values(
}
impl InventoryManager {
pub(crate) async fn native_upload_thumbnail(
&self,
inventory_id: UUID,
task_id: UUID,
j2c_image_data: Vec<u8>,
cancellation_token: Option<CancellationToken>,
progress: Option<
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
>,
) -> Result<Option<Option<UUID>>, Error> {
if inventory_id == UUID::zero()
|| j2c_image_data.is_empty()
|| j2c_image_data.len() > crate::asset_models::MAX_ASSET_BYTES
{
return Err(Error::Argument);
}
let token = cancellation_token.unwrap_or_default();
token.throw_if_cancellation_requested()?;
let client = self.client()?;
let Some(simulator) = client.native_network()?.current_sim() else {
return Ok(Some(None));
};
let Some(uri) = self
.wait_for_capability(&simulator, "InventoryThumbnailUpload", &token)
.await?
else {
return Ok(Some(None));
};
let inventory_store = self.native_store();
let known_local_folder = task_id == UUID::zero()
&& inventory_store
.as_ref()
.and_then(|store| {
store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(inventory_id)
.ok()
.flatten()
})
.is_some();
let metadata = if known_local_folder {
HashMap::from([("category_id".to_owned(), OSD::UUID(inventory_id))])
} else {
let mut metadata = HashMap::from([("item_id".to_owned(), OSD::UUID(inventory_id))]);
if task_id != UUID::zero() {
metadata.insert("task_id".to_owned(), OSD::UUID(task_id));
}
metadata
};
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(metadata))?;
let (metadata_response, metadata_body) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
payload,
token.clone(),
None,
)
.await?;
if !(200..300).contains(&metadata_response.status_code) {
return Ok(Some(None));
}
let metadata: serde_json::Value =
serde_json::from_slice(&metadata_body).map_err(|_| Error::Argument)?;
let Some(uploader) = metadata.get("uploader").and_then(serde_json::Value::as_str) else {
return Ok(Some(None));
};
let (upload_response, upload_body) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
Uri(uploader.to_owned()),
"image/x-j2c".into(),
j2c_image_data,
token,
progress,
)
.await?;
if !(200..300).contains(&upload_response.status_code) {
return Ok(Some(None));
}
let result: serde_json::Value =
serde_json::from_slice(&upload_body).map_err(|_| Error::Argument)?;
if result.get("state").and_then(serde_json::Value::as_str) != Some("complete") {
return Ok(Some(None));
}
let asset_id = result
.get("new_asset")
.or_else(|| result.get("new_asset_id"))
.and_then(serde_json::Value::as_str)
.map(|id| UUID::new_with_string(id.to_owned()))
.transpose()?;
Ok(Some(asset_id))
}
pub(crate) async fn native_update_material_inventory(
&self,
material: crate::assets::AssetMaterial,

View File

@@ -5,6 +5,7 @@
#![allow(clippy::needless_pass_by_value)]
use crate::agent_manager::EventRegistry;
use crate::client_core::ClientWeakHandle;
use crate::marketplace::{
MarketplaceFolderRole, MarketplaceListingStatus, MarketplaceValidationFlags,
};
@@ -291,8 +292,13 @@ impl MarketplaceListingsSyncedEventArgs {
}
}
enum MarketplaceClient {
Standalone(GridClient),
ClientOwned(ClientWeakHandle),
}
struct MarketplaceState {
client: GridClient,
client: RwLock<MarketplaceClient>,
by_id: RwLock<HashMap<i32, MarketplaceListing>>,
by_folder: RwLock<HashMap<UUID, MarketplaceListing>>,
errors: EventRegistry<MarketplaceErrorEventArgs>,
@@ -306,15 +312,36 @@ pub struct MarketplaceManager(Arc<MarketplaceState>);
impl MarketplaceManager {
pub fn new(client: GridClient) -> Result<Self, Error> {
Ok(Self(Arc::new(MarketplaceState {
client,
Ok(Self::with_client(MarketplaceClient::Standalone(client)))
}
fn with_client(client: MarketplaceClient) -> Self {
Self(Arc::new(MarketplaceState {
client: RwLock::new(client),
by_id: RwLock::new(HashMap::new()),
by_folder: RwLock::new(HashMap::new()),
errors: EventRegistry::default(),
changed: EventRegistry::default(),
synced: EventRegistry::default(),
live_mutations: AtomicBool::new(false),
})))
}))
}
pub(crate) fn native_new_client_owned(client: &GridClient) -> Self {
Self::with_client(MarketplaceClient::ClientOwned(client.native_weak_handle()))
}
pub(crate) fn native_bind_to_client(&self, client: &GridClient) {
*write(&self.0.client) = MarketplaceClient::ClientOwned(client.native_weak_handle());
}
fn client(&self) -> Result<GridClient, Error> {
match &*read(&self.0.client) {
MarketplaceClient::Standalone(client) => Ok(client.clone()),
MarketplaceClient::ClientOwned(client) => {
client.upgrade().ok_or(Error::InvalidOperation)
}
}
}
/// Explicitly enables mutation requests to non-test capability hosts.
@@ -366,8 +393,7 @@ impl MarketplaceManager {
async fn base_uri(&self, token: &CancellationToken) -> Result<Uri, Error> {
token.throw_if_cancellation_requested()?;
let simulator = self
.0
.client
.client()?
.native_network()?
.native_current_sim()
.ok_or(Error::InvalidOperation)?;
@@ -412,7 +438,7 @@ impl MarketplaceManager {
return Err(Error::InvalidOperation);
}
let uri = Uri(format!("{}/{}", base.0.trim_end_matches('/'), path));
let client = self.0.client.native_http_caps_client();
let client = self.client()?.native_http_caps_client();
match method {
"GET" => client.get(uri, token, None).await,
"POST" => {

View File

@@ -10,15 +10,15 @@ use libremetaverse::packets::{
SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket,
};
use libremetaverse::{
CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient, LoginState,
LoginStatus, NetworkManager, NetworkManagerDisconnectType, NetworkManagerLoginResponseCallback,
PacketEventDictionary, Simulator,
CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient,
InventoryItem, LoginState, LoginStatus, NetworkManager, NetworkManagerDisconnectType,
NetworkManagerLoginResponseCallback, PacketEventDictionary, Simulator,
};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{
CancellationTokenSource, EventHandler, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use libremetaverse_types::{UUID, Utils};
use libremetaverse_types::{AssetType, Error, UUID, Utils};
use std::collections::{BTreeMap, HashMap};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
@@ -1843,12 +1843,17 @@ async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task
client.settings().agent_settings_mut().multiple_sims = true;
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
let manager = client.network();
let manager_event_order = Arc::new(Mutex::new(Vec::new()));
let (queue_sender, queue_receiver) = channel();
let queue_order = Arc::clone(&manager_event_order);
let _queue_guard = manager.subscribe_event_queue_running(Arc::new(move |args| {
queue_order.lock().unwrap().push("event_queue_running");
queue_sender.send(args.simulator().handle).unwrap();
}));
let (logout_sender, logout_receiver) = channel();
let logout_order = Arc::clone(&manager_event_order);
let _logout_guard = manager.subscribe_logged_out(Arc::new(move |args| {
logout_order.lock().unwrap().push("logged_out");
logout_sender.send(args.inventory_items).unwrap();
}));
@@ -1929,6 +1934,77 @@ async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task
assert_eq!(asset, b"fake-grid-asset");
assert_eq!(downloads.active_download_count(), 0);
// Cross-manager construction is deliberately performed only after the
// authenticated simulator and seed capabilities are available. Every
// manager below shares the same client runtime and must preserve its
// local store when accessed again through GridClient.
{
let agent = client.self_();
assert_eq!(
agent.agent_id(),
UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap()
);
assert_eq!(
agent.session_id(),
UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap()
);
}
let inventory_id =
UUID::new_with_string("44444444-4444-4444-4444-444444444444".into()).unwrap();
let inventory = client.inventory();
let mut store = inventory.store().expect("authenticated inventory store");
let mut item = InventoryItem::new_with_uuid(inventory_id).unwrap();
item.base.set_name("fake-grid inventory marker".into());
store.set_item(inventory_id, item.base);
assert!(
client
.inventory()
.store()
.unwrap()
.contains_with_uuid(inventory_id)
.unwrap()
);
assert!(!client.ais_client().is_available());
assert_eq!(client.appearance().last_update_received_cof_version(), -1);
assert!(
client
.assets()
.create_asset_wrapper(AssetType::Texture)
.is_ok()
);
let _avatars = client.avatars();
assert!(client.animesh().get_player(UUID::zero()).unwrap().is_none());
client.objects().request_object(first.clone(), 1).unwrap();
assert!(client.parcels().current_parcel().is_none());
assert!(client.grid().regions_read_only().is_empty());
assert!(client.friends().friend_list().is_empty());
assert_eq!(client.groups().current_group_count(), -1);
assert!(client.marketplace().listings_by_id().is_empty());
let _terrain = client.terrain();
let _environment = client.environment();
let _sound = client.sound();
let _estate = client.estate();
let cancelled_search = CancellationTokenSource::new();
cancelled_search.cancel();
assert!(matches!(
client
.directory()
.search_people("Ada Lovelace".into(), Some(cancelled_search.token()),),
Err(Error::Cancelled)
));
let (marketplace_error_sender, marketplace_error_receiver) = channel();
let marketplace_order = Arc::clone(&manager_event_order);
let _marketplace_error_guard = client.marketplace().subscribe_error(Arc::new(move |_| {
marketplace_order.lock().unwrap().push("marketplace_error");
marketplace_error_sender.send(()).unwrap();
}));
client.marketplace().fetch_listings(None).await.unwrap();
marketplace_error_receiver
.recv_timeout(Duration::from_secs(1))
.unwrap();
second_server.commands.send(ServerCommand::Handoff).unwrap();
manager
.connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32(
@@ -2007,6 +2083,13 @@ async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task
assert!(downloads.is_disposed());
assert!(!downloads.dispatcher_running());
assert_eq!(downloads.active_download_count(), 0);
assert_eq!(
*manager_event_order.lock().unwrap(),
["event_queue_running", "marketplace_error", "logged_out"]
);
let client_cancellation = client.cancellation_token();
client.dispose_with_method().unwrap();
assert!(client_cancellation.is_cancellation_requested());
let uris = request_uris.lock().unwrap();
assert!(uris.iter().any(|uri| uri.contains("/login")));
assert!(uris.iter().any(|uri| uri.contains("/seed")));