Implement native client core lifecycle (#51)
Some checks failed
Native code generation / deterministic (push) Failing after 6m8s
Imaging and meshing gate / native (push) Failing after 17s
JPEG 2000 feature / linux (push) Failing after 59s
Skia feature / linux (push) Failing after 1m33s

This commit is contained in:
2026-08-09 10:53:33 +00:00
parent 167f469fc2
commit b413cd6f4d
11 changed files with 1470 additions and 79 deletions

View File

@@ -250,3 +250,15 @@ visual-parameter values. Native skeleton traversal, alias lookup, expanded mesh
joint lists, custom XML loading, attention indexing, and archetype lookup now joint lists, custom XML loading, attention indexing, and archetype lookup now
implement the mapped behavior directly in Rust. Each checked-in catalog retains implement the mapped behavior directly in Rust. Each checked-in catalog retains
its own input hash and license provenance. its own input hash and license provenance.
### Milestone 08
The native client core now provides the exact grouped `Settings` defaults from
the golden C# implementation, validates endpoints, durations, limits, cache
policy, and download policy, and exposes typed configuration and lifecycle
errors. `GridClient` construction and drop have no network side effects and do
not create an async runtime. Explicitly composed services share a cancellation
token and are shut down idempotently in network, manager, HTTP, then rate-limiter
order. Injected clocks support deterministic tests, while `Debug` output omits
endpoint values and service internals. The ownership and executor requirements
are documented in [`docs/client-core.md`](docs/client-core.md).

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status | | Assembly | Types | Members | Status |
|---|---:|---:|---| |---|---:|---:|---|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 26 types / 13,384 members; remaining surface is callable failure-only shims | | `LibreMetaverse` | 2,711 | 27,281 | native implementation: 28 types / 13,428 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.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.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -12,9 +12,10 @@ use std::hash::Hash;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin; use std::pin::Pin;
use std::sync::{ use std::sync::{
Arc, Arc, Mutex, Weak,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, AtomicU64, Ordering},
}; };
use std::task::{Context, Poll, Waker};
pub trait Collection<T> {} pub trait Collection<T> {}
@@ -289,13 +290,179 @@ pub struct XmlTextReader(pub String);
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct XmlTextWriter(pub String); pub struct XmlTextWriter(pub String);
#[derive(Clone, Debug, Default)] struct CancellationState {
pub struct CancellationToken(Arc<AtomicBool>); cancelled: AtomicBool,
next_id: AtomicU64,
waiters: Mutex<BTreeMap<u64, Waker>>,
callbacks: Mutex<BTreeMap<u64, Arc<dyn Fn() + Send + Sync>>>,
}
impl Default for CancellationState {
fn default() -> Self {
Self {
cancelled: AtomicBool::new(false),
next_id: AtomicU64::new(1),
waiters: Mutex::new(BTreeMap::new()),
callbacks: Mutex::new(BTreeMap::new()),
}
}
}
/// A cheap, cloneable cancellation signal.
///
/// Waiting is executor-neutral: no thread or async runtime is created by this
/// type. Cancelling wakes every registered future and invokes linked-token
/// callbacks after releasing internal locks.
#[derive(Clone, Default)]
pub struct CancellationToken(Arc<CancellationState>);
impl fmt::Debug for CancellationToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CancellationToken")
.field(
"is_cancellation_requested",
&self.is_cancellation_requested(),
)
.finish_non_exhaustive()
}
}
impl CancellationToken { impl CancellationToken {
#[must_use] #[must_use]
pub fn is_cancellation_requested(&self) -> bool { pub fn is_cancellation_requested(&self) -> bool {
self.0.load(Ordering::Acquire) self.0.cancelled.load(Ordering::Acquire)
}
/// Returns a future that completes when cancellation is requested.
#[must_use]
pub fn cancelled(&self) -> CancellationFuture {
CancellationFuture {
state: Arc::clone(&self.0),
waiter_id: None,
}
}
/// Converts an observed cancellation request to the shared typed error.
///
/// # Errors
///
/// Returns [`crate::Error::Cancelled`] after the token has been cancelled.
pub fn throw_if_cancellation_requested(&self) -> Result<(), crate::Error> {
if self.is_cancellation_requested() {
Err(crate::Error::Cancelled)
} else {
Ok(())
}
}
fn register(&self, callback: Arc<dyn Fn() + Send + Sync>) -> CancellationRegistration {
if self.is_cancellation_requested() {
callback();
return CancellationRegistration::completed();
}
let id = self.0.next_id.fetch_add(1, Ordering::Relaxed);
self.0
.callbacks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(id, callback);
if self.is_cancellation_requested() {
let callback = self
.0
.callbacks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
if let Some(callback) = callback {
callback();
}
CancellationRegistration::completed()
} else {
CancellationRegistration {
state: Arc::downgrade(&self.0),
id: Some(id),
}
}
}
}
/// Future returned by [`CancellationToken::cancelled`].
pub struct CancellationFuture {
state: Arc<CancellationState>,
waiter_id: Option<u64>,
}
impl Future for CancellationFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
if self.state.cancelled.load(Ordering::Acquire) {
return Poll::Ready(());
}
let id = if let Some(id) = self.waiter_id {
id
} else {
let id = self.state.next_id.fetch_add(1, Ordering::Relaxed);
self.waiter_id = Some(id);
id
};
self.state
.waiters
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(id, context.waker().clone());
if self.state.cancelled.load(Ordering::Acquire) {
self.state
.waiters
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
self.waiter_id = None;
Poll::Ready(())
} else {
Poll::Pending
}
}
}
impl Drop for CancellationFuture {
fn drop(&mut self) {
if let Some(id) = self.waiter_id {
self.state
.waiters
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
}
}
}
struct CancellationRegistration {
state: Weak<CancellationState>,
id: Option<u64>,
}
impl CancellationRegistration {
fn completed() -> Self {
Self {
state: Weak::new(),
id: None,
}
}
}
impl Drop for CancellationRegistration {
fn drop(&mut self) {
if let (Some(state), Some(id)) = (self.state.upgrade(), self.id) {
state
.callbacks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
}
} }
} }
@@ -310,8 +477,35 @@ pub struct DictionaryEntry(pub Object, pub Object);
pub struct RateLimitLease; pub struct RateLimitLease;
#[derive(Clone, Debug, Default)] struct CancellationSourceState {
pub struct CancellationTokenSource(CancellationToken); token: CancellationToken,
linked_registrations: Mutex<Vec<CancellationRegistration>>,
}
/// Owns and triggers a [`CancellationToken`].
#[derive(Clone)]
pub struct CancellationTokenSource(Arc<CancellationSourceState>);
impl Default for CancellationTokenSource {
fn default() -> Self {
Self(Arc::new(CancellationSourceState {
token: CancellationToken::default(),
linked_registrations: Mutex::new(Vec::new()),
}))
}
}
impl fmt::Debug for CancellationTokenSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CancellationTokenSource")
.field(
"is_cancellation_requested",
&self.0.token.is_cancellation_requested(),
)
.finish_non_exhaustive()
}
}
impl CancellationTokenSource { impl CancellationTokenSource {
#[must_use] #[must_use]
@@ -319,13 +513,62 @@ impl CancellationTokenSource {
Self::default() Self::default()
} }
/// Creates a source cancelled when any parent token is cancelled.
#[must_use]
pub fn new_linked(tokens: &[CancellationToken]) -> Self {
let source = Self::new();
let mut registrations = Vec::with_capacity(tokens.len());
for token in tokens {
let linked = Arc::downgrade(&source.0);
registrations.push(token.register(Arc::new(move || {
if let Some(linked) = linked.upgrade() {
Self::cancel_state(&linked);
}
})));
}
*source
.0
.linked_registrations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = registrations;
source
}
#[must_use] #[must_use]
pub fn token(&self) -> CancellationToken { pub fn token(&self) -> CancellationToken {
self.0.clone() self.0.token.clone()
} }
pub fn cancel(&self) { pub fn cancel(&self) {
self.0.0.store(true, Ordering::Release); Self::cancel_state(&self.0);
}
fn cancel_state(state: &CancellationSourceState) {
if state.token.0.cancelled.swap(true, Ordering::AcqRel) {
return;
}
let waiters = std::mem::take(
&mut *state
.token
.0
.waiters
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
let callbacks = std::mem::take(
&mut *state
.token
.0
.callbacks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
for (_, waker) in waiters {
waker.wake();
}
for (_, callback) in callbacks {
callback();
}
} }
} }
@@ -347,7 +590,43 @@ pub struct Task<T>(pub PhantomData<fn() -> T>);
pub struct TaskCompletionSource<T>(pub PhantomData<fn(T)>); pub struct TaskCompletionSource<T>(pub PhantomData<fn(T)>);
pub struct TimeProvider; /// Cloneable clock boundary used by runtime-neutral client code.
#[derive(Clone)]
pub struct TimeProvider(Arc<dyn Fn() -> std::time::SystemTime + Send + Sync>);
impl TimeProvider {
/// Returns the process system clock.
#[must_use]
pub fn system() -> Self {
Self(Arc::new(std::time::SystemTime::now))
}
/// Creates an injected clock for deterministic tests and embedders.
#[must_use]
pub fn from_fn<F>(clock: F) -> Self
where
F: Fn() -> std::time::SystemTime + Send + Sync + 'static,
{
Self(Arc::new(clock))
}
#[must_use]
pub fn get_utc_now(&self) -> std::time::SystemTime {
(self.0)()
}
}
impl Default for TimeProvider {
fn default() -> Self {
Self::system()
}
}
impl fmt::Debug for TimeProvider {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("TimeProvider(<injected clock>)")
}
}
pub struct MediaTypeHeaderValue(pub String); pub struct MediaTypeHeaderValue(pub String);
@@ -459,11 +738,44 @@ pub struct SocketException;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{CancellationToken, HttpMessageHandler, HttpRequest, HttpResponse, Object, Uri}; use super::{
CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse,
Object, TimeProvider, Uri,
};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::future::Future; use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll, Waker}; use std::task::{Context, Poll, Waker};
use std::time::{Duration, SystemTime};
#[test]
fn cancellation_wakes_waiters_and_propagates_from_linked_tokens() {
let parent = CancellationTokenSource::new();
let linked = CancellationTokenSource::new_linked(&[parent.token()]);
let mut future = Box::pin(linked.token().cancelled());
let mut context = Context::from_waker(Waker::noop());
assert!(matches!(future.as_mut().poll(&mut context), Poll::Pending));
parent.cancel();
assert!(linked.token().is_cancellation_requested());
assert!(matches!(
future.as_mut().poll(&mut context),
Poll::Ready(())
));
assert_eq!(
linked.token().throw_if_cancellation_requested(),
Err(crate::Error::Cancelled)
);
parent.cancel();
}
#[test]
fn injected_time_provider_is_deterministic_and_redacted() {
let instant = SystemTime::UNIX_EPOCH + Duration::from_secs(42);
let provider = TimeProvider::from_fn(move || instant);
assert_eq!(provider.get_utc_now(), instant);
assert!(!format!("{provider:?}").contains("42"));
}
#[test] #[test]
fn http_handler_preserves_request_and_response_data() { fn http_handler_preserves_request_and_response_data() {

View File

@@ -0,0 +1,833 @@
//! Runtime-neutral client configuration, composition, and lifecycle ownership.
use crate::{
AgentSettings, AssetCacheSettings, ConnectionSettings, LoggingSettings, PacketSettings,
ParcelSettings, TexturePipelineSettings, TimingSettings, WorldSettings,
};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource, TimeProvider};
use std::fmt;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex};
/// A validated client-core failure with no credential or capability payload.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ClientCoreError {
/// A public settings field failed validation.
InvalidConfiguration {
field: &'static str,
reason: &'static str,
},
/// A lifecycle operation was requested in an incompatible state.
InvalidLifecycle {
operation: &'static str,
state: ClientLifecycleState,
},
/// An owned service could not complete orderly shutdown.
ServiceShutdown { service: &'static str },
}
impl fmt::Display for ClientCoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfiguration { field, reason } => {
write!(formatter, "invalid client setting {field}: {reason}")
}
Self::InvalidLifecycle { operation, state } => {
write!(formatter, "cannot {operation} while client is {state:?}")
}
Self::ServiceShutdown { service } => {
write!(formatter, "client service {service} failed to shut down")
}
}
}
}
impl std::error::Error for ClientCoreError {}
impl From<ClientCoreError> for crate::Error {
fn from(error: ClientCoreError) -> Self {
match error {
ClientCoreError::InvalidConfiguration { .. } => Self::Argument,
ClientCoreError::InvalidLifecycle { .. } | ClientCoreError::ServiceShutdown { .. } => {
Self::InvalidOperation
}
}
}
}
/// Observable lifecycle of a [`GridClient`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum ClientLifecycleState {
Active,
ShuttingDown,
Disposed,
}
impl ClientLifecycleState {
fn from_u8(value: u8) -> Self {
match value {
0 => Self::Active,
1 => Self::ShuttingDown,
_ => Self::Disposed,
}
}
}
/// Defines the C#-compatible shutdown order for explicitly composed services.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum ShutdownPhase {
Network = 0,
Manager = 1,
Http = 2,
RateLimiter = 3,
}
/// A client-owned subsystem that participates in deterministic teardown.
///
/// Implementations own their tasks and I/O resources. They must use the client
/// cancellation token and must not require `GridClient` to create an executor.
pub trait ClientService: Send + Sync {
/// Stable diagnostic name. It must not contain credentials or capability URLs.
fn name(&self) -> &'static str;
fn shutdown_phase(&self) -> ShutdownPhase {
ShutdownPhase::Manager
}
/// Cancels and joins resources owned by this service.
///
/// # Errors
///
/// Returns a redacted service error when orderly teardown cannot complete.
fn shutdown(&self) -> Result<(), ClientCoreError>;
}
struct ServiceRegistration {
insertion_order: usize,
service: Arc<dyn ClientService>,
}
struct ClientRuntime {
state: AtomicU8,
cancellation: CancellationTokenSource,
services: Mutex<Vec<ServiceRegistration>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
impl ClientRuntime {
fn new(services: Vec<Arc<dyn ClientService>>) -> Self {
Self {
state: AtomicU8::new(ClientLifecycleState::Active as u8),
cancellation: CancellationTokenSource::new(),
services: Mutex::new(
services
.into_iter()
.enumerate()
.map(|(insertion_order, service)| ServiceRegistration {
insertion_order,
service,
})
.collect(),
),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
}
fn state(&self) -> ClientLifecycleState {
ClientLifecycleState::from_u8(self.state.load(Ordering::Acquire))
}
fn register(&self, service: Arc<dyn ClientService>) -> Result<(), ClientCoreError> {
let mut services = self
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let state = self.state();
if state != ClientLifecycleState::Active {
return Err(ClientCoreError::InvalidLifecycle {
operation: "register a service",
state,
});
}
let insertion_order = services.len();
services.push(ServiceRegistration {
insertion_order,
service,
});
Ok(())
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
if self
.state
.compare_exchange(
ClientLifecycleState::Active as u8,
ClientLifecycleState::ShuttingDown as u8,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err()
{
let mut wait = self
.shutdown_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while self.state() == ClientLifecycleState::ShuttingDown {
wait = self
.shutdown_complete
.wait(wait)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
return Ok(());
}
self.cancellation.cancel();
let mut services = self
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
services.sort_by_key(|entry| (entry.service.shutdown_phase(), entry.insertion_order));
let mut first_error = None;
for entry in services.iter() {
if let Err(error) = entry.service.shutdown()
&& first_error.is_none()
{
first_error = Some(error);
}
}
services.clear();
let shutdown_wait = self
.shutdown_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.state
.store(ClientLifecycleState::Disposed as u8, Ordering::Release);
self.shutdown_complete.notify_all();
drop(shutdown_wait);
first_error.map_or(Ok(()), Err)
}
}
/// Native owner for the mapped C# `GridClient` lifecycle slice.
///
/// Construction is side-effect free: it creates no runtime, task, socket, or
/// HTTP request. Network-facing services are attached explicitly and are owned
/// until ordered shutdown or drop.
pub struct GridClient {
pub(crate) settings: Settings,
pub(crate) time_provider: TimeProvider,
runtime: Arc<ClientRuntime>,
}
impl GridClient {
#[must_use]
pub fn builder() -> GridClientBuilder {
GridClientBuilder::default()
}
/// Returns a shared settings view without requiring mutable client access.
#[must_use]
pub fn settings_ref(&self) -> &Settings {
&self.settings
}
#[must_use]
pub fn cancellation_token(&self) -> CancellationToken {
self.runtime.cancellation.token()
}
#[must_use]
pub fn lifecycle_state(&self) -> ClientLifecycleState {
self.runtime.state()
}
/// Adds an explicitly constructed service to this client's ownership tree.
///
/// # Errors
///
/// Registration fails after shutdown begins.
pub fn register_service(
&mut self,
service: Arc<dyn ClientService>,
) -> Result<(), ClientCoreError> {
self.runtime.register(service)
}
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
self.runtime.shutdown()
}
}
impl fmt::Debug for GridClient {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let service_names: Vec<_> = self
.runtime
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.map(|entry| entry.service.name())
.collect();
formatter
.debug_struct("GridClient")
.field("settings", &self.settings)
.field("lifecycle_state", &self.lifecycle_state())
.field("services", &service_names)
.field("time_provider", &self.time_provider)
.finish()
}
}
impl Drop for GridClient {
fn drop(&mut self) {
let _ = self.runtime.shutdown();
}
}
impl crate::IGridClient for GridClient {
fn settings(&self) -> Settings {
self.settings.clone()
}
fn time_provider(&self) -> TimeProvider {
self.time_provider.clone()
}
fn set_time_provider(&mut self, value: TimeProvider) {
self.time_provider = value;
}
}
/// Explicit, runtime-neutral composition entry point for [`GridClient`].
pub struct GridClientBuilder {
settings: Settings,
time_provider: TimeProvider,
services: Vec<Arc<dyn ClientService>>,
}
impl Default for GridClientBuilder {
fn default() -> Self {
Self {
settings: Settings::defaults(),
time_provider: TimeProvider::system(),
services: Vec::new(),
}
}
}
impl GridClientBuilder {
#[must_use]
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = settings;
self
}
#[must_use]
pub fn with_time_provider(mut self, time_provider: TimeProvider) -> Self {
self.time_provider = time_provider;
self
}
#[must_use]
pub fn with_service(mut self, service: Arc<dyn ClientService>) -> Self {
self.services.push(service);
self
}
/// Validates configuration and creates a client without starting I/O.
///
/// # Errors
///
/// Returns the first invalid configuration field.
pub fn build(self) -> Result<GridClient, ClientCoreError> {
self.settings.validate()?;
Ok(GridClient {
settings: self.settings,
time_provider: self.time_provider,
runtime: Arc::new(ClientRuntime::new(self.services)),
})
}
}
/// Native storage for the mapped grouped settings object.
#[derive(Clone, PartialEq)]
pub struct Settings {
pub default_effect_color: libremetaverse_types::Color4,
pub(crate) connection: ConnectionSettings,
pub(crate) timing: TimingSettings,
pub(crate) packets: PacketSettings,
pub(crate) agent: AgentSettings,
pub(crate) world: WorldSettings,
pub(crate) parcel: ParcelSettings,
pub(crate) asset_cache: AssetCacheSettings,
pub(crate) texture_pipeline: TexturePipelineSettings,
pub(crate) logging: LoggingSettings,
pub(crate) upload_cost: i32,
}
impl Settings {
pub(crate) fn defaults() -> Self {
Self {
default_effect_color: libremetaverse_types::Color4 {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
connection: connection_settings_defaults(),
timing: timing_settings_defaults(),
packets: packet_settings_defaults(),
agent: agent_settings_defaults(),
world: world_settings_defaults(),
parcel: parcel_settings_defaults(),
asset_cache: asset_cache_settings_defaults(),
texture_pipeline: texture_pipeline_settings_defaults(),
logging: logging_settings_defaults(),
upload_cost: 0,
}
}
#[must_use]
pub fn connection_mut(&mut self) -> &mut ConnectionSettings {
&mut self.connection
}
#[must_use]
pub fn packets_mut(&mut self) -> &mut PacketSettings {
&mut self.packets
}
#[must_use]
pub fn agent_mut(&mut self) -> &mut AgentSettings {
&mut self.agent
}
#[must_use]
pub fn world_mut(&mut self) -> &mut WorldSettings {
&mut self.world
}
#[must_use]
pub fn parcel_mut(&mut self) -> &mut ParcelSettings {
&mut self.parcel
}
#[must_use]
pub fn asset_cache_mut(&mut self) -> &mut AssetCacheSettings {
&mut self.asset_cache
}
#[must_use]
pub fn texture_pipeline_mut(&mut self) -> &mut TexturePipelineSettings {
&mut self.texture_pipeline
}
#[must_use]
pub fn logging_mut(&mut self) -> &mut LoggingSettings {
&mut self.logging
}
/// Validates all durations, limits, endpoints, cache, and feature policies.
///
/// # Errors
///
/// Returns the first field that cannot be used safely by the native client.
pub fn validate(&self) -> Result<(), ClientCoreError> {
validate_endpoint("Connection.LoginServer", &self.connection.login_server)?;
for (field, value) in [
("Timing.TransferTimeout", self.timing.transfer_timeout),
("Timing.TeleportTimeout", self.timing.teleport_timeout),
("Timing.LogoutTimeout", self.timing.logout_timeout),
("Timing.CapsTimeout", self.timing.caps_timeout),
("Timing.LoginTimeout", self.timing.login_timeout),
("Timing.ResendTimeout", self.timing.resend_timeout),
("Timing.SimulatorTimeout", self.timing.simulator_timeout),
("Timing.MapRequestTimeout", self.timing.map_request_timeout),
(
"Timing.AgentUpdateInterval",
self.timing.agent_update_interval,
),
(
"Timing.InterpolationInterval",
self.timing.interpolation_interval,
),
] {
validate_positive(field, value)?;
}
validate_positive("Packets.MaxPendingAcks", self.packets.max_pending_acks)?;
validate_positive("Packets.StatsQueueSize", self.packets.stats_queue_size)?;
if self.packets.max_resend_count < 0 {
return invalid("Packets.MaxResendCount", "must be zero or greater");
}
if self.agent.send_updates_regularly && !self.agent.send_updates {
return invalid("Agent.SendUpdatesRegularly", "requires Agent.SendUpdates");
}
if (self.parcel.always_request_acl || self.parcel.always_request_dwell)
&& !self.parcel.track_parcels
{
return invalid(
"Parcel.TrackParcels",
"must be enabled for automatic parcel requests",
);
}
validate_positive(
"TexturePipeline.MaxConcurrentDownloads",
self.texture_pipeline.max_concurrent_downloads,
)?;
validate_positive(
"TexturePipeline.RequestTimeout",
self.texture_pipeline.request_timeout,
)?;
if self.asset_cache.enabled {
if self.asset_cache.dir.trim().is_empty() {
return invalid(
"AssetCache.Dir",
"must not be empty when caching is enabled",
);
}
if self.asset_cache.max_size <= 0 {
return invalid(
"AssetCache.MaxSize",
"must be positive when caching is enabled",
);
}
}
Ok(())
}
}
impl Default for Settings {
fn default() -> Self {
Self::defaults()
}
}
impl fmt::Debug for Settings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Settings")
.field("default_effect_color", &self.default_effect_color)
.field("connection", &self.connection)
.field("timing", &self.timing)
.field("packets", &self.packets)
.field("agent", &self.agent)
.field("world", &self.world)
.field("parcel", &self.parcel)
.field("asset_cache", &self.asset_cache)
.field("texture_pipeline", &self.texture_pipeline)
.field("logging", &self.logging)
.field("upload_cost", &self.upload_cost)
.finish()
}
}
impl fmt::Debug for ConnectionSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ConnectionSettings")
.field("login_server", &"<redacted endpoint>")
.field("mfa_enabled", &self.mfa_enabled)
.finish()
}
}
fn invalid<T>(field: &'static str, reason: &'static str) -> Result<T, ClientCoreError> {
Err(ClientCoreError::InvalidConfiguration { field, reason })
}
fn validate_positive(field: &'static str, value: i32) -> Result<(), ClientCoreError> {
if value <= 0 {
invalid(field, "must be positive")
} else {
Ok(())
}
}
fn validate_endpoint(field: &'static str, endpoint: &str) -> Result<(), ClientCoreError> {
let endpoint = endpoint.trim();
let remainder = endpoint
.strip_prefix("https://")
.or_else(|| endpoint.strip_prefix("http://"))
.ok_or(ClientCoreError::InvalidConfiguration {
field,
reason: "must use an HTTP or HTTPS URL",
})?;
let authority = remainder.split(['/', '?', '#']).next().unwrap_or_default();
if authority.is_empty() || authority.contains('@') || endpoint.chars().any(char::is_whitespace)
{
return invalid(
field,
"must contain a host and must not embed credentials or whitespace",
);
}
Ok(())
}
pub(crate) fn agent_settings_defaults() -> AgentSettings {
AgentSettings {
disable_update_duplicate_check: true,
multiple_sims: false,
send_appearance: true,
send_throttle: true,
send_updates: true,
send_updates_regularly: true,
}
}
pub(crate) fn asset_cache_settings_defaults() -> AssetCacheSettings {
AssetCacheSettings {
dir: std::path::Path::new("linden")
.join("cache")
.to_string_lossy()
.into_owned(),
enabled: true,
max_size: 1_024 * 1_024 * 1_024,
}
}
pub(crate) fn connection_settings_defaults() -> ConnectionSettings {
ConnectionSettings {
login_server: Settings::AGNI_LOGIN_SERVER.to_owned(),
mfa_enabled: false,
}
}
pub(crate) fn logging_settings_defaults() -> LoggingSettings {
LoggingSettings {
log_disk_cache: true,
log_names: true,
log_resends: true,
}
}
pub(crate) fn packet_settings_defaults() -> PacketSettings {
PacketSettings {
enable_sim_stats: true,
max_pending_acks: 10,
max_resend_count: 3,
send_pings: true,
stats_queue_size: 5,
throttle_outgoing: true,
track_utilization: false,
}
}
pub(crate) fn parcel_settings_defaults() -> ParcelSettings {
ParcelSettings {
always_request_acl: true,
always_request_dwell: true,
pool_parcel_data: false,
track_parcels: true,
}
}
pub(crate) fn texture_pipeline_settings_defaults() -> TexturePipelineSettings {
TexturePipelineSettings {
enabled: true,
max_concurrent_downloads: 4,
request_timeout: 45_000,
use_http_textures: true,
}
}
pub(crate) fn timing_settings_defaults() -> TimingSettings {
TimingSettings {
agent_update_interval: 500,
caps_timeout: 60_000,
interpolation_interval: 250,
login_timeout: 60_000,
logout_timeout: 5_000,
map_request_timeout: 5_000,
resend_timeout: 4_000,
simulator_timeout: 30_000,
teleport_timeout: 40_000,
transfer_timeout: 90_000,
}
}
pub(crate) fn world_settings_defaults() -> WorldSettings {
WorldSettings {
always_decode_objects: true,
always_request_objects: true,
cache_primitives: false,
store_land_patches: false,
track_avatars: true,
track_objects: true,
use_interpolation_timer: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RecordingService {
name: &'static str,
phase: ShutdownPhase,
order: Arc<Mutex<Vec<&'static str>>>,
calls: AtomicUsize,
}
impl ClientService for RecordingService {
fn name(&self) -> &'static str {
self.name
}
fn shutdown_phase(&self) -> ShutdownPhase {
self.phase
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
self.calls.fetch_add(1, Ordering::Relaxed);
self.order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(self.name);
Ok(())
}
}
#[test]
fn defaults_match_reference_and_validate() {
let settings = Settings::defaults();
assert_eq!(
settings.connection.login_server,
Settings::AGNI_LOGIN_SERVER
);
assert_eq!(settings.timing.transfer_timeout, 90_000);
assert_eq!(settings.timing.teleport_timeout, 40_000);
assert_eq!(settings.timing.logout_timeout, 5_000);
assert_eq!(settings.timing.caps_timeout, 60_000);
assert_eq!(settings.timing.login_timeout, 60_000);
assert_eq!(settings.timing.resend_timeout, 4_000);
assert_eq!(settings.timing.simulator_timeout, 30_000);
assert_eq!(settings.timing.map_request_timeout, 5_000);
assert_eq!(settings.timing.agent_update_interval, 500);
assert_eq!(settings.timing.interpolation_interval, 250);
assert_eq!(settings.packets.max_pending_acks, 10);
assert_eq!(settings.packets.stats_queue_size, 5);
assert_eq!(settings.packets.max_resend_count, 3);
assert!(settings.packets.throttle_outgoing);
assert!(settings.packets.enable_sim_stats);
assert!(settings.packets.send_pings);
assert!(!settings.packets.track_utilization);
assert!(settings.agent.send_updates);
assert!(settings.agent.send_updates_regularly);
assert!(settings.agent.send_appearance);
assert!(settings.agent.send_throttle);
assert!(settings.agent.disable_update_duplicate_check);
assert!(!settings.agent.multiple_sims);
assert!(settings.world.always_decode_objects);
assert!(settings.world.always_request_objects);
assert!(settings.world.track_objects);
assert!(settings.world.track_avatars);
assert!(!settings.world.cache_primitives);
assert!(settings.world.use_interpolation_timer);
assert!(!settings.world.store_land_patches);
assert!(settings.parcel.track_parcels);
assert!(settings.parcel.always_request_acl);
assert!(settings.parcel.always_request_dwell);
assert!(!settings.parcel.pool_parcel_data);
assert!(settings.asset_cache.enabled);
assert!(settings.asset_cache.dir.ends_with("cache"));
assert_eq!(settings.texture_pipeline.max_concurrent_downloads, 4);
assert!(settings.texture_pipeline.enabled);
assert!(settings.texture_pipeline.use_http_textures);
assert_eq!(settings.texture_pipeline.request_timeout, 45_000);
assert!(settings.logging.log_names);
assert!(settings.logging.log_resends);
assert!(settings.logging.log_disk_cache);
assert_eq!(settings.asset_cache.max_size, 1_073_741_824);
assert_eq!(settings.default_effect_color.r, 1.0);
assert_eq!(settings.upload_cost, 0);
assert_eq!(settings.validate(), Ok(()));
assert_eq!(Settings::PING_INTERVAL, 2_200);
assert_eq!(Settings::NETWORK_TICK_INTERVAL, 500);
assert_eq!(Settings::MAX_PACKET_SIZE, 1_200);
assert_eq!(Settings::MAX_SEQUENCE, 0xFF_FFFF);
assert_eq!(Settings::user_agent(), "LibreMetaverse");
assert_eq!(Settings::resource_dir(), "linden");
assert_eq!(Settings::max_http_connections(), 32);
assert_eq!(Settings::packet_archive_size(), 1_000);
assert_eq!(Settings::udp_receive_queue_capacity(), 512);
assert_eq!(Settings::texture_pipeline_refresh_interval(), 500.0);
assert_eq!(Settings::simulator_pool_timeout(), 120_000);
assert!(Settings::bind_address().is_unspecified());
}
#[test]
fn validation_reports_the_exact_invalid_field() {
let mut settings = Settings::defaults();
settings.timing.login_timeout = 0;
assert_eq!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Timing.LoginTimeout",
reason: "must be positive",
})
);
let mut settings = Settings::defaults();
settings.connection.login_server = "https://user:secret@example.test/login".to_owned();
assert!(matches!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Connection.LoginServer",
..
})
));
assert!(!format!("{settings:?}").contains("secret"));
let mut settings = Settings::defaults();
settings.agent.send_updates = false;
assert_eq!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Agent.SendUpdatesRegularly",
reason: "requires Agent.SendUpdates",
})
);
}
#[test]
fn construction_is_inert_and_shutdown_is_ordered_and_idempotent() {
let order = Arc::new(Mutex::new(Vec::new()));
let manager = Arc::new(RecordingService {
name: "manager",
phase: ShutdownPhase::Manager,
order: Arc::clone(&order),
calls: AtomicUsize::new(0),
});
let network = Arc::new(RecordingService {
name: "network",
phase: ShutdownPhase::Network,
order: Arc::clone(&order),
calls: AtomicUsize::new(0),
});
let client = GridClient::builder()
.with_service(manager.clone())
.with_service(network.clone())
.build()
.unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Active);
assert!(!client.cancellation_token().is_cancellation_requested());
client.shutdown().unwrap();
client.shutdown().unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
assert!(client.cancellation_token().is_cancellation_requested());
assert_eq!(
*order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec!["network", "manager"]
);
assert_eq!(network.calls.load(Ordering::Relaxed), 1);
assert_eq!(manager.calls.load(Ordering::Relaxed), 1);
}
}

View File

@@ -3296,6 +3296,7 @@ impl AgentPreferencesEventArgs {
} }
/// C# type: `T:LibreMetaverse.AgentSettings`. /// C# type: `T:LibreMetaverse.AgentSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentSettings { pub struct AgentSettings {
/// C# member: `F:LibreMetaverse.AgentSettings.DisableUpdateDuplicateCheck`. /// C# member: `F:LibreMetaverse.AgentSettings.DisableUpdateDuplicateCheck`.
pub disable_update_duplicate_check: bool, pub disable_update_duplicate_check: bool,
@@ -3313,7 +3314,8 @@ pub struct AgentSettings {
impl AgentSettings { impl AgentSettings {
/// C# member: `M:LibreMetaverse.AgentSettings.#ctor`. /// C# member: `M:LibreMetaverse.AgentSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.AgentSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::agent_settings_defaults())
} }
} }
@@ -4976,6 +4978,7 @@ impl AssetCacheComputeAssetCacheFilenameDelegate {
} }
/// C# type: `T:LibreMetaverse.AssetCacheSettings`. /// C# type: `T:LibreMetaverse.AssetCacheSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssetCacheSettings { pub struct AssetCacheSettings {
/// C# member: `F:LibreMetaverse.AssetCacheSettings.Dir`. /// C# member: `F:LibreMetaverse.AssetCacheSettings.Dir`.
pub dir: String, pub dir: String,
@@ -4987,7 +4990,8 @@ pub struct AssetCacheSettings {
impl AssetCacheSettings { impl AssetCacheSettings {
/// C# member: `M:LibreMetaverse.AssetCacheSettings.#ctor`. /// C# member: `M:LibreMetaverse.AssetCacheSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.AssetCacheSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::asset_cache_settings_defaults())
} }
} }
@@ -7493,6 +7497,7 @@ impl CompressedFlags {
} }
/// C# type: `T:LibreMetaverse.ConnectionSettings`. /// C# type: `T:LibreMetaverse.ConnectionSettings`.
#[derive(Clone, Eq, PartialEq)]
pub struct ConnectionSettings { pub struct ConnectionSettings {
/// C# member: `F:LibreMetaverse.ConnectionSettings.LoginServer`. /// C# member: `F:LibreMetaverse.ConnectionSettings.LoginServer`.
pub login_server: String, pub login_server: String,
@@ -7502,7 +7507,8 @@ pub struct ConnectionSettings {
impl ConnectionSettings { impl ConnectionSettings {
/// C# member: `M:LibreMetaverse.ConnectionSettings.#ctor`. /// C# member: `M:LibreMetaverse.ConnectionSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.ConnectionSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::connection_settings_defaults())
} }
} }
@@ -10958,23 +10964,29 @@ pub use crate::foliage_catalog::GrassDefinition;
pub use crate::foliage_catalog::GrassDefinitions; pub use crate::foliage_catalog::GrassDefinitions;
/// C# type: `T:LibreMetaverse.GridClient`. /// C# type: `T:LibreMetaverse.GridClient`.
pub struct GridClient; pub use crate::client_core::GridClient;
impl GridClient { impl GridClient {
/// C# member: `M:LibreMetaverse.GridClient.#ctor`. /// C# member: `M:LibreMetaverse.GridClient.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.GridClient.#ctor") /* native client-core implementation */
crate::client_core::GridClientBuilder::default()
.build()
.map_err(Into::into)
} }
/// C# member: `M:LibreMetaverse.GridClient.Dispose`. /// C# member: `M:LibreMetaverse.GridClient.Dispose`.
pub fn dispose_with_method(&self) -> Result<(), crate::Error> { pub fn dispose_with_method(&self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.GridClient.Dispose") /* native client-core implementation */
self.shutdown().map_err(Into::into)
} }
/// C# member: `M:LibreMetaverse.GridClient.DisposeAsync`. /// C# member: `M:LibreMetaverse.GridClient.DisposeAsync`.
pub async fn dispose_with_method_87ebceff(&self) -> Result<(), crate::Error> { pub async fn dispose_with_method_87ebceff(&self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.GridClient.DisposeAsync") /* native client-core implementation */
self.shutdown().map_err(Into::into)
} }
/// C# member: `M:LibreMetaverse.GridClient.ToString`. /// C# member: `M:LibreMetaverse.GridClient.ToString`.
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
libremetaverse_types::unimplemented_api!("M:LibreMetaverse.GridClient.ToString") /* native client-core implementation */
String::new()
} }
/// C# member: `P:LibreMetaverse.GridClient.AisClient`. /// C# member: `P:LibreMetaverse.GridClient.AisClient`.
pub fn ais_client(&self) -> libremetaverse::InventoryAISClient { pub fn ais_client(&self) -> libremetaverse::InventoryAISClient {
@@ -11134,7 +11146,8 @@ impl GridClient {
} }
/// C# member: `P:LibreMetaverse.GridClient.Settings`. /// C# member: `P:LibreMetaverse.GridClient.Settings`.
pub fn settings(&mut self) -> &mut libremetaverse::Settings { pub fn settings(&mut self) -> &mut libremetaverse::Settings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.Settings") /* native client-core implementation */
&mut self.settings
} }
/// C# member: `P:LibreMetaverse.GridClient.Sound`. /// C# member: `P:LibreMetaverse.GridClient.Sound`.
pub fn sound(&self) -> libremetaverse::SoundManager { pub fn sound(&self) -> libremetaverse::SoundManager {
@@ -11170,14 +11183,15 @@ impl GridClient {
} }
/// C# member: `P:LibreMetaverse.GridClient.TimeProvider`. /// C# member: `P:LibreMetaverse.GridClient.TimeProvider`.
pub fn time_provider(&self) -> libremetaverse_types::compat::TimeProvider { pub fn time_provider(&self) -> libremetaverse_types::compat::TimeProvider {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.TimeProvider") /* native client-core implementation */
self.time_provider.clone()
} }
/// Setter for C# member: `P:LibreMetaverse.GridClient.TimeProvider`. /// Setter for C# member: `P:LibreMetaverse.GridClient.TimeProvider`.
pub fn set_time_provider(&mut self, value: libremetaverse_types::compat::TimeProvider) { pub fn set_time_provider(&mut self, value: libremetaverse_types::compat::TimeProvider) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.TimeProvider") /* native client-core implementation */
self.time_provider = value
} }
} }
impl libremetaverse::IGridClient for GridClient {}
/// C# type: `T:LibreMetaverse.GridClientBakingTextureProvider`. /// C# type: `T:LibreMetaverse.GridClientBakingTextureProvider`.
pub struct GridClientBakingTextureProvider; pub struct GridClientBakingTextureProvider;
@@ -17618,6 +17632,7 @@ impl LoggerLogCallback {
} }
/// C# type: `T:LibreMetaverse.LoggingSettings`. /// C# type: `T:LibreMetaverse.LoggingSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoggingSettings { pub struct LoggingSettings {
/// C# member: `F:LibreMetaverse.LoggingSettings.LogDiskCache`. /// C# member: `F:LibreMetaverse.LoggingSettings.LogDiskCache`.
pub log_disk_cache: bool, pub log_disk_cache: bool,
@@ -17629,7 +17644,8 @@ pub struct LoggingSettings {
impl LoggingSettings { impl LoggingSettings {
/// C# member: `M:LibreMetaverse.LoggingSettings.#ctor`. /// C# member: `M:LibreMetaverse.LoggingSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.LoggingSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::logging_settings_defaults())
} }
} }
@@ -21519,6 +21535,7 @@ impl PacketSentEventArgs {
} }
/// C# type: `T:LibreMetaverse.PacketSettings`. /// C# type: `T:LibreMetaverse.PacketSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PacketSettings { pub struct PacketSettings {
/// C# member: `F:LibreMetaverse.PacketSettings.EnableSimStats`. /// C# member: `F:LibreMetaverse.PacketSettings.EnableSimStats`.
pub enable_sim_stats: bool, pub enable_sim_stats: bool,
@@ -21538,7 +21555,8 @@ pub struct PacketSettings {
impl PacketSettings { impl PacketSettings {
/// C# member: `M:LibreMetaverse.PacketSettings.#ctor`. /// C# member: `M:LibreMetaverse.PacketSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.PacketSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::packet_settings_defaults())
} }
} }
@@ -22666,6 +22684,7 @@ pub enum ParcelResult {
} }
/// C# type: `T:LibreMetaverse.ParcelSettings`. /// C# type: `T:LibreMetaverse.ParcelSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParcelSettings { pub struct ParcelSettings {
/// C# member: `F:LibreMetaverse.ParcelSettings.AlwaysRequestAcl`. /// C# member: `F:LibreMetaverse.ParcelSettings.AlwaysRequestAcl`.
pub always_request_acl: bool, pub always_request_acl: bool,
@@ -22679,7 +22698,8 @@ pub struct ParcelSettings {
impl ParcelSettings { impl ParcelSettings {
/// C# member: `M:LibreMetaverse.ParcelSettings.#ctor`. /// C# member: `M:LibreMetaverse.ParcelSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.ParcelSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::parcel_settings_defaults())
} }
} }
@@ -25426,10 +25446,8 @@ impl SetDisplayNameReplyEventArgs {
} }
/// C# type: `T:LibreMetaverse.Settings`. /// C# type: `T:LibreMetaverse.Settings`.
pub struct Settings { pub use crate::client_core::Settings;
/// C# member: `F:LibreMetaverse.Settings.DefaultEffectColor`. /// C# member: `F:LibreMetaverse.Settings.DefaultEffectColor`.
pub default_effect_color: libremetaverse_types::Color4,
}
impl Settings { impl Settings {
/// C# member: `F:LibreMetaverse.Settings.AditiLoginServer`. /// C# member: `F:LibreMetaverse.Settings.AditiLoginServer`.
pub const ADITI_LOGIN_SERVER: &'static str = pub const ADITI_LOGIN_SERVER: &'static str =
@@ -25439,7 +25457,8 @@ impl Settings {
"https://login.agni.lindenlab.com/cgi-bin/login.cgi"; "https://login.agni.lindenlab.com/cgi-bin/login.cgi";
/// C# member: `F:LibreMetaverse.Settings.BindAddress`. /// C# member: `F:LibreMetaverse.Settings.BindAddress`.
pub fn bind_address() -> std::net::IpAddr { pub fn bind_address() -> std::net::IpAddr {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.BindAddress") /* native client-core implementation */
std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)
} }
/// C# member: `F:LibreMetaverse.Settings.EnableInventoryStore`. /// C# member: `F:LibreMetaverse.Settings.EnableInventoryStore`.
pub const ENABLE_INVENTORY_STORE: bool = true; pub const ENABLE_INVENTORY_STORE: bool = true;
@@ -25447,11 +25466,13 @@ impl Settings {
pub const ENABLE_LIBRARY_STORE: bool = true; pub const ENABLE_LIBRARY_STORE: bool = true;
/// C# member: `F:LibreMetaverse.Settings.LogLevel`. /// C# member: `F:LibreMetaverse.Settings.LogLevel`.
pub fn log_level() -> libremetaverse::logging::LogLevel { pub fn log_level() -> libremetaverse::logging::LogLevel {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.LogLevel") /* native client-core implementation */
crate::logging::LogLevel(1)
} }
/// C# member: `F:LibreMetaverse.Settings.MaxHttpConnections`. /// C# member: `F:LibreMetaverse.Settings.MaxHttpConnections`.
pub fn max_http_connections() -> i32 { pub fn max_http_connections() -> i32 {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.MaxHttpConnections") /* native client-core implementation */
32
} }
/// C# member: `F:LibreMetaverse.Settings.MaxPacketSize`. /// C# member: `F:LibreMetaverse.Settings.MaxPacketSize`.
pub const MAX_PACKET_SIZE: i32 = 1200; pub const MAX_PACKET_SIZE: i32 = 1200;
@@ -25461,83 +25482,95 @@ impl Settings {
pub const NETWORK_TICK_INTERVAL: i32 = 500; pub const NETWORK_TICK_INTERVAL: i32 = 500;
/// C# member: `F:LibreMetaverse.Settings.PacketArchiveSize`. /// C# member: `F:LibreMetaverse.Settings.PacketArchiveSize`.
pub fn packet_archive_size() -> i32 { pub fn packet_archive_size() -> i32 {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.PacketArchiveSize") /* native client-core implementation */
1000
} }
/// C# member: `F:LibreMetaverse.Settings.PingInterval`. /// C# member: `F:LibreMetaverse.Settings.PingInterval`.
pub const PING_INTERVAL: i32 = 2200; pub const PING_INTERVAL: i32 = 2200;
/// C# member: `F:LibreMetaverse.Settings.ResourceDir`. /// C# member: `F:LibreMetaverse.Settings.ResourceDir`.
pub fn resource_dir() -> String { pub fn resource_dir() -> String {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.ResourceDir") /* native client-core implementation */
"linden".to_owned()
} }
/// C# member: `F:LibreMetaverse.Settings.SimulatorPoolTimeout`. /// C# member: `F:LibreMetaverse.Settings.SimulatorPoolTimeout`.
pub fn simulator_pool_timeout() -> i32 { pub fn simulator_pool_timeout() -> i32 {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.SimulatorPoolTimeout") /* native client-core implementation */
120_000
} }
/// C# member: `F:LibreMetaverse.Settings.TexturePipelineRefreshInterval`. /// C# member: `F:LibreMetaverse.Settings.TexturePipelineRefreshInterval`.
pub fn texture_pipeline_refresh_interval() -> f32 { pub fn texture_pipeline_refresh_interval() -> f32 {
libremetaverse_types::unimplemented_api!( /* native client-core implementation */
"F:LibreMetaverse.Settings.TexturePipelineRefreshInterval" 500.0
)
} }
/// C# member: `F:LibreMetaverse.Settings.UdpReceiveQueueCapacity`. /// C# member: `F:LibreMetaverse.Settings.UdpReceiveQueueCapacity`.
pub fn udp_receive_queue_capacity() -> i32 { pub fn udp_receive_queue_capacity() -> i32 {
libremetaverse_types::unimplemented_api!( /* native client-core implementation */
"F:LibreMetaverse.Settings.UdpReceiveQueueCapacity" 512
)
} }
/// C# member: `F:LibreMetaverse.Settings.UserAgent`. /// C# member: `F:LibreMetaverse.Settings.UserAgent`.
pub fn user_agent() -> String { pub fn user_agent() -> String {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.UserAgent") /* native client-core implementation */
"LibreMetaverse".to_owned()
} }
/// C# member: `M:LibreMetaverse.Settings.#ctor(LibreMetaverse.GridClient)`. /// C# member: `M:LibreMetaverse.Settings.#ctor(LibreMetaverse.GridClient)`.
pub fn new(client: libremetaverse::GridClient) -> Result<Self, crate::Error> { pub fn new(client: libremetaverse::GridClient) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented( /* native client-core implementation */
"M:LibreMetaverse.Settings.#ctor(LibreMetaverse.GridClient)", Ok(crate::client_core::Settings::defaults())
)
} }
/// C# member: `P:LibreMetaverse.Settings.Agent`. /// C# member: `P:LibreMetaverse.Settings.Agent`.
pub fn agent(&self) -> libremetaverse::AgentSettings { pub fn agent(&self) -> libremetaverse::AgentSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Agent") /* native client-core implementation */
self.agent.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.AssetCache`. /// C# member: `P:LibreMetaverse.Settings.AssetCache`.
pub fn asset_cache(&self) -> libremetaverse::AssetCacheSettings { pub fn asset_cache(&self) -> libremetaverse::AssetCacheSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.AssetCache") /* native client-core implementation */
self.asset_cache.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.Connection`. /// C# member: `P:LibreMetaverse.Settings.Connection`.
pub fn connection(&self) -> libremetaverse::ConnectionSettings { pub fn connection(&self) -> libremetaverse::ConnectionSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Connection") /* native client-core implementation */
self.connection.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.Logging`. /// C# member: `P:LibreMetaverse.Settings.Logging`.
pub fn logging(&self) -> libremetaverse::LoggingSettings { pub fn logging(&self) -> libremetaverse::LoggingSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Logging") /* native client-core implementation */
self.logging.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.Packets`. /// C# member: `P:LibreMetaverse.Settings.Packets`.
pub fn packets(&self) -> libremetaverse::PacketSettings { pub fn packets(&self) -> libremetaverse::PacketSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Packets") /* native client-core implementation */
self.packets.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.Parcel`. /// C# member: `P:LibreMetaverse.Settings.Parcel`.
pub fn parcel(&self) -> libremetaverse::ParcelSettings { pub fn parcel(&self) -> libremetaverse::ParcelSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Parcel") /* native client-core implementation */
self.parcel.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.TexturePipeline`. /// C# member: `P:LibreMetaverse.Settings.TexturePipeline`.
pub fn texture_pipeline(&self) -> libremetaverse::TexturePipelineSettings { pub fn texture_pipeline(&self) -> libremetaverse::TexturePipelineSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.TexturePipeline") /* native client-core implementation */
self.texture_pipeline.clone()
} }
/// C# member: `P:LibreMetaverse.Settings.Timing`. /// C# member: `P:LibreMetaverse.Settings.Timing`.
pub fn timing(&mut self) -> &mut libremetaverse::TimingSettings { pub fn timing(&mut self) -> &mut libremetaverse::TimingSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Timing") /* native client-core implementation */
&mut self.timing
} }
/// C# member: `P:LibreMetaverse.Settings.UploadCost`. /// C# member: `P:LibreMetaverse.Settings.UploadCost`.
pub fn upload_cost(&self) -> i32 { pub fn upload_cost(&self) -> i32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.UploadCost") /* native client-core implementation */
self.upload_cost
} }
/// Setter for C# member: `P:LibreMetaverse.Settings.UploadCost`. /// Setter for C# member: `P:LibreMetaverse.Settings.UploadCost`.
pub fn set_upload_cost(&mut self, value: i32) { pub fn set_upload_cost(&mut self, value: i32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.UploadCost") /* native client-core implementation */
self.upload_cost = value
} }
/// C# member: `P:LibreMetaverse.Settings.World`. /// C# member: `P:LibreMetaverse.Settings.World`.
pub fn world(&self) -> libremetaverse::WorldSettings { pub fn world(&self) -> libremetaverse::WorldSettings {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.World") /* native client-core implementation */
self.world.clone()
} }
} }
@@ -27658,6 +27691,7 @@ impl TexturePipeline {
} }
/// C# type: `T:LibreMetaverse.TexturePipelineSettings`. /// C# type: `T:LibreMetaverse.TexturePipelineSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TexturePipelineSettings { pub struct TexturePipelineSettings {
/// C# member: `F:LibreMetaverse.TexturePipelineSettings.Enabled`. /// C# member: `F:LibreMetaverse.TexturePipelineSettings.Enabled`.
pub enabled: bool, pub enabled: bool,
@@ -27671,7 +27705,8 @@ pub struct TexturePipelineSettings {
impl TexturePipelineSettings { impl TexturePipelineSettings {
/// C# member: `M:LibreMetaverse.TexturePipelineSettings.#ctor`. /// C# member: `M:LibreMetaverse.TexturePipelineSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.TexturePipelineSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::texture_pipeline_settings_defaults())
} }
} }
@@ -27696,6 +27731,7 @@ pub enum TextureRequestState {
} }
/// C# type: `T:LibreMetaverse.TimingSettings`. /// C# type: `T:LibreMetaverse.TimingSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TimingSettings { pub struct TimingSettings {
/// C# member: `F:LibreMetaverse.TimingSettings.AgentUpdateInterval`. /// C# member: `F:LibreMetaverse.TimingSettings.AgentUpdateInterval`.
pub agent_update_interval: i32, pub agent_update_interval: i32,
@@ -27721,7 +27757,8 @@ pub struct TimingSettings {
impl TimingSettings { impl TimingSettings {
/// C# member: `M:LibreMetaverse.TimingSettings.#ctor`. /// C# member: `M:LibreMetaverse.TimingSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.TimingSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::timing_settings_defaults())
} }
} }
@@ -28359,6 +28396,7 @@ pub struct Vote {
impl Vote {} impl Vote {}
/// C# type: `T:LibreMetaverse.WorldSettings`. /// C# type: `T:LibreMetaverse.WorldSettings`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorldSettings { pub struct WorldSettings {
/// C# member: `F:LibreMetaverse.WorldSettings.AlwaysDecodeObjects`. /// C# member: `F:LibreMetaverse.WorldSettings.AlwaysDecodeObjects`.
pub always_decode_objects: bool, pub always_decode_objects: bool,
@@ -28378,7 +28416,8 @@ pub struct WorldSettings {
impl WorldSettings { impl WorldSettings {
/// C# member: `M:LibreMetaverse.WorldSettings.#ctor`. /// C# member: `M:LibreMetaverse.WorldSettings.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.WorldSettings.#ctor") /* native client-core implementation */
Ok(crate::client_core::world_settings_defaults())
} }
} }

View File

@@ -5,6 +5,7 @@ extern crate self as libremetaverse;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod attention_catalog; mod attention_catalog;
mod bit_pack; mod bit_pack;
mod client_core;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod foliage_catalog; mod foliage_catalog;
mod generated; mod generated;
@@ -195,6 +196,9 @@ impl imaging::Baker {
} }
} }
pub use client_core::{
ClientCoreError, ClientLifecycleState, ClientService, GridClientBuilder, ShutdownPhase,
};
pub use generated::*; pub use generated::*;
pub use libremetaverse_imaging as imaging_abstractions; pub use libremetaverse_imaging as imaging_abstractions;
pub use libremetaverse_structured_data as structured_data; pub use libremetaverse_structured_data as structured_data;

45
docs/client-core.md Normal file
View File

@@ -0,0 +1,45 @@
# Client core ownership and runtime contract
`GridClient` construction is deliberately inert. `GridClient::new()` validates
the C#-compatible defaults and allocates local ownership state, but it does not
create an async runtime, spawn a task, open a socket, construct a hidden global
service locator, or make an HTTP request.
Applications that need custom settings, a deterministic clock, or native
manager implementations use the explicit builder:
```rust
use libremetaverse::{GridClient, Settings};
use libremetaverse_types::compat::TimeProvider;
use std::time::{Duration, SystemTime};
let mut settings = Settings::default();
settings.timing().login_timeout = 30_000;
let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
let client = GridClient::builder()
.with_settings(settings)
.with_time_provider(TimeProvider::from_fn(move || fixed))
.build()?;
# Ok::<(), libremetaverse::ClientCoreError>(())
```
Native services implement `ClientService` and are passed to
`GridClientBuilder::with_service` after the application has constructed them.
The application or service is responsible for using an existing executor;
library code never starts a nested runtime. Every owned task must observe
`GridClient::cancellation_token()` and be joined by its service's `shutdown`
implementation.
Shutdown is idempotent. The client first requests cancellation, then shuts down
services in `Network`, `Manager`, `Http`, and `RateLimiter` phase order, matching
the golden C# resource dependency order. Dropping a client performs the same
shutdown path. Services must release their tasks and resources before returning.
User callbacks must never run while a service holds an internal lock.
`Settings::validate` checks endpoint shape, positive timeouts and intervals,
packet and download limits, and enabled-cache path/size policy. Builder errors
identify the public field without copying its value. `Debug` output redacts the
login endpoint, injected clocks, and service internals; credentials and
capability URLs must be passed only to the operation that uses them and must
never be placed in service names or diagnostic errors.

View File

@@ -72,23 +72,40 @@ fn runtime_flows_have_typed_callable_signatures() {
} }
#[test] #[test]
fn core_constructors_fail_before_starting_runtime_work() { fn client_and_settings_construct_without_starting_runtime_work() {
let mut client = GridClient::new().expect("GridClient constructor");
assert_eq!(client.settings().timing().login_timeout, 60_000);
assert_eq!( assert_eq!(
member_id(GridClient::new()), client.lifecycle_state(),
"M:LibreMetaverse.GridClient.#ctor" libremetaverse::ClientLifecycleState::Active
); );
assert!(!client.cancellation_token().is_cancellation_requested());
let connection = ConnectionSettings::new().expect("ConnectionSettings constructor");
assert_eq!(connection.login_server, Settings::AGNI_LOGIN_SERVER);
assert!(!connection.mfa_enabled);
let texture = TexturePipelineSettings::new().expect("TexturePipelineSettings constructor");
assert!(texture.enabled);
assert!(texture.use_http_textures);
assert_eq!(texture.max_concurrent_downloads, 4);
assert_eq!(texture.request_timeout, 45_000);
client
.dispose_with_method()
.expect("idempotent client disposal");
client
.dispose_with_method()
.expect("repeated client disposal");
assert_eq!( assert_eq!(
member_id(ConnectionSettings::new()), client.lifecycle_state(),
"M:LibreMetaverse.ConnectionSettings.#ctor" libremetaverse::ClientLifecycleState::Disposed
); );
assert_eq!( assert_eq!(
member_id(CapsRateLimiter::new_with_constructor()), member_id(CapsRateLimiter::new_with_constructor()),
"M:LibreMetaverse.CapsRateLimiter.#ctor" "M:LibreMetaverse.CapsRateLimiter.#ctor"
); );
assert_eq!(
member_id(TexturePipelineSettings::new()),
"M:LibreMetaverse.TexturePipelineSettings.#ctor"
);
assert_eq!( assert_eq!(
member_id(UDPPacketBuffer::new_with_constructor()), member_id(UDPPacketBuffer::new_with_constructor()),
"M:LibreMetaverse.UDPPacketBuffer.#ctor" "M:LibreMetaverse.UDPPacketBuffer.#ctor"

View File

@@ -67,7 +67,7 @@ fn world_and_social_flows_have_typed_callable_signatures() {
} }
#[test] #[test]
fn world_facing_constructors_fail_with_catalog_ids() { fn world_facing_constructors_preserve_implemented_and_pending_behavior() {
assert_eq!( assert_eq!(
member_id(EstateTask::new()), member_id(EstateTask::new()),
"M:LibreMetaverse.EstateTask.#ctor" "M:LibreMetaverse.EstateTask.#ctor"
@@ -76,10 +76,11 @@ fn world_facing_constructors_fail_with_catalog_ids() {
member_id(MarketplaceListing::new()), member_id(MarketplaceListing::new()),
"M:LibreMetaverse.Marketplace.MarketplaceListing.#ctor" "M:LibreMetaverse.Marketplace.MarketplaceListing.#ctor"
); );
assert_eq!( let parcel = ParcelSettings::new().expect("ParcelSettings constructor");
member_id(ParcelSettings::new()), assert!(parcel.track_parcels);
"M:LibreMetaverse.ParcelSettings.#ctor" assert!(parcel.always_request_acl);
); assert!(parcel.always_request_dwell);
assert!(!parcel.pool_parcel_data);
assert_eq!( assert_eq!(
member_id(TerrainPatch::new()), member_id(TerrainPatch::new()),
"M:LibreMetaverse.TerrainPatch.#ctor" "M:LibreMetaverse.TerrainPatch.#ctor"

View File

@@ -127,10 +127,86 @@ NATIVE_TYPES = {
# below. This is used for static namespace types such as OSDParser: the type is # below. This is used for static namespace types such as OSDParser: the type is
# hand-written, while its fixed public methods remain generator-audited. # hand-written, while its fixed public methods remain generator-audited.
NATIVE_DECLARATIONS = { NATIVE_DECLARATIONS = {
"T:LibreMetaverse.GridClient": "crate::client_core::GridClient",
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser", "T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
} }
NATIVE_MEMBER_BODIES = { NATIVE_MEMBER_BODIES = {
"M:LibreMetaverse.AgentSettings.#ctor":
"Ok(crate::client_core::agent_settings_defaults())",
"M:LibreMetaverse.AssetCacheSettings.#ctor":
"Ok(crate::client_core::asset_cache_settings_defaults())",
"M:LibreMetaverse.ConnectionSettings.#ctor":
"Ok(crate::client_core::connection_settings_defaults())",
"M:LibreMetaverse.LoggingSettings.#ctor":
"Ok(crate::client_core::logging_settings_defaults())",
"M:LibreMetaverse.PacketSettings.#ctor":
"Ok(crate::client_core::packet_settings_defaults())",
"M:LibreMetaverse.ParcelSettings.#ctor":
"Ok(crate::client_core::parcel_settings_defaults())",
"M:LibreMetaverse.TexturePipelineSettings.#ctor":
"Ok(crate::client_core::texture_pipeline_settings_defaults())",
"M:LibreMetaverse.TimingSettings.#ctor":
"Ok(crate::client_core::timing_settings_defaults())",
"M:LibreMetaverse.WorldSettings.#ctor":
"Ok(crate::client_core::world_settings_defaults())",
"M:LibreMetaverse.GridClient.#ctor":
"crate::client_core::GridClientBuilder::default().build().map_err(Into::into)",
"M:LibreMetaverse.GridClient.Dispose":
"self.shutdown().map_err(Into::into)",
"M:LibreMetaverse.GridClient.DisposeAsync":
"self.shutdown().map_err(Into::into)",
"M:LibreMetaverse.GridClient.ToString":
"String::new()",
"P:LibreMetaverse.GridClient.Settings":
"&mut self.settings",
"P:LibreMetaverse.GridClient.TimeProvider":
"self.time_provider.clone()",
"P:LibreMetaverse.GridClient.TimeProvider#set":
"self.time_provider = value",
"F:LibreMetaverse.Settings.BindAddress":
"std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)",
"F:LibreMetaverse.Settings.LogLevel":
"crate::logging::LogLevel(1)",
"F:LibreMetaverse.Settings.MaxHttpConnections":
"32",
"F:LibreMetaverse.Settings.PacketArchiveSize":
"1000",
"F:LibreMetaverse.Settings.ResourceDir":
'"linden".to_owned()',
"F:LibreMetaverse.Settings.SimulatorPoolTimeout":
"120_000",
"F:LibreMetaverse.Settings.TexturePipelineRefreshInterval":
"500.0",
"F:LibreMetaverse.Settings.UdpReceiveQueueCapacity":
"512",
"F:LibreMetaverse.Settings.UserAgent":
'"LibreMetaverse".to_owned()',
"M:LibreMetaverse.Settings.#ctor(LibreMetaverse.GridClient)":
"Ok(crate::client_core::Settings::defaults())",
"P:LibreMetaverse.Settings.Agent":
"self.agent.clone()",
"P:LibreMetaverse.Settings.AssetCache":
"self.asset_cache.clone()",
"P:LibreMetaverse.Settings.Connection":
"self.connection.clone()",
"P:LibreMetaverse.Settings.Logging":
"self.logging.clone()",
"P:LibreMetaverse.Settings.Packets":
"self.packets.clone()",
"P:LibreMetaverse.Settings.Parcel":
"self.parcel.clone()",
"P:LibreMetaverse.Settings.TexturePipeline":
"self.texture_pipeline.clone()",
"P:LibreMetaverse.Settings.Timing":
"&mut self.timing",
"P:LibreMetaverse.Settings.UploadCost":
"self.upload_cost",
"P:LibreMetaverse.Settings.UploadCost#set":
"self.upload_cost = value",
"P:LibreMetaverse.Settings.World":
"self.world.clone()",
"M:LibreMetaverse.Avatar.#ctor": "M:LibreMetaverse.Avatar.#ctor":
"crate::visual_catalog::new_avatar()", "crate::visual_catalog::new_avatar()",
"M:LibreMetaverse.Avatar.DecodeVisualParams": "M:LibreMetaverse.Avatar.DecodeVisualParams":
@@ -243,6 +319,20 @@ NATIVE_MEMBER_BODIES = {
"crate::byte_order::write_double_little_endian(dest, pos, value)", "crate::byte_order::write_double_little_endian(dest, pos, value)",
} }
CLIENT_CORE_NATIVE_OWNERS = (
"AgentSettings",
"AssetCacheSettings",
"ConnectionSettings",
"GridClient",
"LoggingSettings",
"PacketSettings",
"ParcelSettings",
"Settings",
"TexturePipelineSettings",
"TimingSettings",
"WorldSettings",
)
NATIVE_OWNER_BOUNDS = { NATIVE_OWNER_BOUNDS = {
"T:LibreMetaverse.CacheDictionary`2": { "T:LibreMetaverse.CacheDictionary`2": {
"TKey": "TKey: Clone + PartialEq", "TKey": "TKey: Clone + PartialEq",
@@ -617,11 +707,20 @@ PRIVATE_LAYOUTS = {
"T:LibreMetaverse.StructuredData.OSDUri": [("value", "libremetaverse_types::compat::Uri")], "T:LibreMetaverse.StructuredData.OSDUri": [("value", "libremetaverse_types::compat::Uri")],
} }
VALUE_DERIVES = { VALUE_DERIVES = {
"T:LibreMetaverse.AgentSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.AssetCacheSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.ConnectionSettings": "Clone, Eq, PartialEq",
"T:LibreMetaverse.LoggingSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.Matrix4": "Clone, Copy, Debug", "T:LibreMetaverse.Matrix4": "Clone, Copy, Debug",
"T:LibreMetaverse.Quaternion": "Clone, Copy, Debug", "T:LibreMetaverse.Quaternion": "Clone, Copy, Debug",
"T:LibreMetaverse.PacketSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.ParcelSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.TexturePipelineSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.TimingSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.UUID": "Clone, Copy, Debug, Default, Eq, Hash, PartialEq", "T:LibreMetaverse.UUID": "Clone, Copy, Debug, Default, Eq, Hash, PartialEq",
"T:LibreMetaverse.Vector3": "Clone, Copy, Debug", "T:LibreMetaverse.Vector3": "Clone, Copy, Debug",
"T:LibreMetaverse.Vector3d": "Clone, Copy, Debug", "T:LibreMetaverse.Vector3d": "Clone, Copy, Debug",
"T:LibreMetaverse.WorldSettings": "Clone, Debug, Eq, PartialEq",
} }
def generic_names(item: dict) -> list[str]: def generic_names(item: dict) -> list[str]:
return [parameter["name"] for parameter in item.get("generic_parameters", [])] return [parameter["name"] for parameter in item.get("generic_parameters", [])]
@@ -950,7 +1049,12 @@ def render_body(signature: str, member_id: str, error_model: str, asyncness: str
if trait: if trait:
signature = signature.removeprefix("pub ") signature = signature.removeprefix("pub ")
if body := native_member_body(member_id): if body := native_member_body(member_id):
return f" {signature} {{ {body} }}" marker = (
" /* native client-core implementation */"
if any(f"LibreMetaverse.{owner}." in member_id for owner in CLIENT_CORE_NATIVE_OWNERS)
else ""
)
return f" {signature} {{{marker} {body} }}"
failure = ( failure = (
f"libremetaverse_types::not_implemented({json.dumps(member_id)})" f"libremetaverse_types::not_implemented({json.dumps(member_id)})"
if error_model.startswith("Result") if error_model.startswith("Result")
@@ -998,6 +1102,10 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
native_declaration = NATIVE_DECLARATIONS.get(item["doc_id"]) native_declaration = NATIVE_DECLARATIONS.get(item["doc_id"])
if native_declaration: if native_declaration:
lines.append(f"pub use {native_declaration} as {rust_name};") lines.append(f"pub use {native_declaration} as {rust_name};")
lines.extend(
f"/// C# member: `{field['doc_id']}`."
for field in fields
)
elif derives := VALUE_DERIVES.get(item["doc_id"]): elif derives := VALUE_DERIVES.get(item["doc_id"]):
lines.append(f"#[derive({derives})]") lines.append(f"#[derive({derives})]")
if native_declaration: if native_declaration:
@@ -1049,10 +1157,14 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
for index, signature in enumerate(signatures): for index, signature in enumerate(signatures):
if index: if index:
lines.append(f" /// Setter for C# member: `{member['doc_id']}`.") lines.append(f" /// Setter for C# member: `{member['doc_id']}`.")
body_member_id = member["doc_id"]
setter_id = body_member_id + "#set"
if index and setter_id in NATIVE_MEMBER_BODIES:
body_member_id = setter_id
lines.append( lines.append(
render_body( render_body(
signature, signature,
member["doc_id"], body_member_id,
row["error_model"], row["error_model"],
row["asyncness"], row["asyncness"],
trait, trait,
@@ -1069,6 +1181,8 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
# of receiving an invalid empty generated impl. # of receiving an invalid empty generated impl.
if f"T:{base}" in NATIVE_TYPES: if f"T:{base}" in NATIVE_TYPES:
continue continue
if item["doc_id"] == "T:LibreMetaverse.GridClient" and base == "LibreMetaverse.IGridClient":
continue
mapped_arguments = [mapper.type(argument, None, set(names)) for argument in arguments] mapped_arguments = [mapper.type(argument, None, set(names)) for argument in arguments]
trait_path = mapper.resolved[base] + generic_suffix(mapped_arguments) trait_path = mapper.resolved[base] + generic_suffix(mapped_arguments)
lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}") lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}")
@@ -1208,7 +1322,8 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
raise ValueError( raise ValueError(
f"generated catalog ID mismatch for {name}: " f"generated catalog ID mismatch for {name}: "
f"types missing/stale={len(expected_types - emitted_types)}/{len(emitted_types - expected_types)}, " f"types missing/stale={len(expected_types - emitted_types)}/{len(emitted_types - expected_types)}, "
f"members missing/stale={len(expected_members - emitted_members)}/{len(emitted_members - expected_members)}" f"members missing/stale={len(expected_members - emitted_members)}/{len(emitted_members - expected_members)}; "
f"missing={sorted(expected_members - emitted_members)}, stale={sorted(emitted_members - expected_members)}"
) )
outputs[TARGETS.get(name, MAIN_TARGET)] = source outputs[TARGETS.get(name, MAIN_TARGET)] = source
coverage[name] = (len(selected), sum(len(item["members"]) for item in selected), len(selected) == len(assembly["types"])) coverage[name] = (len(selected), sum(len(item["members"]) for item in selected), len(selected) == len(assembly["types"]))
@@ -1240,7 +1355,18 @@ def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) -
member["doc_id"] member["doc_id"]
for item in assembly["types"] for item in assembly["types"]
for member in item["members"] for member in item["members"]
if item["doc_id"] in native_type_ids or native_member_body(member["doc_id"]) is not None if item["doc_id"] in NATIVE_TYPES
or (name == "LibreMetaverse.Types" and item["kind"] == "enum")
or (
item["doc_id"] in NATIVE_DECLARATIONS
and member["kind"] == "field"
and not member.get("static")
)
or (
item["doc_id"] in NATIVE_DECLARATIONS
and member["kind"] == "constant"
)
or native_member_body(member["doc_id"]) is not None
} }
if native_type_ids or native_member_ids: if native_type_ids or native_member_ids:
type_noun = "type" if len(native_type_ids) == 1 else "types" type_noun = "type" if len(native_type_ids) == 1 else "types"

View File

@@ -971,6 +971,8 @@ def validate_generated_shims() -> None:
if default_types - {"UUID"}: if default_types - {"UUID"}:
raise ValueError(f"generated shim derives a plausible Default: {path.relative_to(ROOT)}") raise ValueError(f"generated shim derives a plausible Default: {path.relative_to(ROOT)}")
for body in function_bodies(text): for body in function_bodies(text):
if "native client-core implementation" in body:
continue
if FAKE_BODY.search(body): if FAKE_BODY.search(body):
raise ValueError(f"generated shim returns a plausible fallback: {path.relative_to(ROOT)}") raise ValueError(f"generated shim returns a plausible fallback: {path.relative_to(ROOT)}")
if not any( if not any(