Implement native asset pipeline and cache (#64)
Some checks failed
Native code generation / deterministic (push) Failing after 2m18s
Imaging and meshing gate / native (push) Failing after 1m30s
JPEG 2000 feature / linux (push) Successful in 2m40s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m8s
Some checks failed
Native code generation / deterministic (push) Failing after 2m18s
Imaging and meshing gate / native (push) Failing after 1m30s
JPEG 2000 feature / linux (push) Successful in 2m40s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m8s
This commit is contained in:
35
Cargo.lock
generated
35
Cargo.lock
generated
@@ -26,6 +26,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aotuv_lancer_vorbis_sys"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bc4fd1a61860d2f1198b60bedd30910eaffa978f1ee6214dfb24ac70d589225"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"ogg_next_sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -853,7 +863,9 @@ dependencies = [
|
|||||||
"os_info",
|
"os_info",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"vorbis_rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1298,6 +1310,15 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ogg_next_sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2d7a48e247c2bb07e633aefb65a38648ea58c7eedd4e4408a5861721ab049b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -2239,6 +2260,20 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vorbis_rs"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49c5da94d280f7a27e8c937e9b73df2da3e23a2583f48471fd8fb4c72f9c1933"
|
||||||
|
dependencies = [
|
||||||
|
"aotuv_lancer_vorbis_sys",
|
||||||
|
"errno",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"ogg_next_sys",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
@@ -358,6 +358,13 @@ and uses same-directory temporary replacement with rollback. The compatibility
|
|||||||
and persistence contract is documented in
|
and persistence contract is documented in
|
||||||
[`docs/inventory.md`](docs/inventory.md).
|
[`docs/inventory.md`](docs/inventory.md).
|
||||||
|
|
||||||
|
Native asset models now enforce bounded format decoding, real JPEG 2000 and Ogg
|
||||||
|
Vorbis codec boundaries, client-owned capability and LLUDP downloads, complete
|
||||||
|
small/Xfer and two-stage capability uploads, independent cancellation for
|
||||||
|
deduplicated HTTP subscribers, and atomic size-pruned disk caching. The ownership,
|
||||||
|
correlation, and corruption contracts are documented in
|
||||||
|
[`docs/assets.md`](docs/assets.md).
|
||||||
|
|
||||||
The client-owned `InventoryManager` now implements the packet-level create,
|
The client-owned `InventoryManager` now implements the packet-level create,
|
||||||
update, move, copy, remove, fetch, search, give, rez/derez, script-state, and
|
update, move, copy, remove, fetch, search, give, rez/derez, script-state, and
|
||||||
task-inventory workflows. Callback IDs and concurrent item/task waiters are
|
task-inventory workflows. Callback IDs and concurrent item/task waiters are
|
||||||
|
|||||||
@@ -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: 166 types / 14,923 members; remaining surface is callable failure-only shims |
|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 197 types / 15,177 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 |
|
||||||
|
|||||||
@@ -223,11 +223,11 @@ impl J2kCodec {
|
|||||||
if options.max_encoded_bytes == 0 {
|
if options.max_encoded_bytes == 0 {
|
||||||
return Err(Error::Argument);
|
return Err(Error::Argument);
|
||||||
}
|
}
|
||||||
if let J2kCompression::Lossy { compression_ratio } = options.compression {
|
if let J2kCompression::Lossy { compression_ratio } = options.compression
|
||||||
if !compression_ratio.is_finite() || compression_ratio < 1.0 {
|
&& (!compression_ratio.is_finite() || compression_ratio < 1.0)
|
||||||
|
{
|
||||||
return Err(Error::Argument);
|
return Err(Error::Argument);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
encode_with_openjpeg(image, options)
|
encode_with_openjpeg(image, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ repository.workspace = true
|
|||||||
description = "Rust rewrite shell for the LibreMetaverse client library"
|
description = "Rust rewrite shell for the LibreMetaverse client library"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["dds-bc67"]
|
default = ["dds-bc67", "jpeg2000"]
|
||||||
dds-bc67 = ["dep:bcdec_rs"]
|
dds-bc67 = ["dep:bcdec_rs"]
|
||||||
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
||||||
|
|
||||||
@@ -24,7 +24,9 @@ mac_address2 = "2.0.2"
|
|||||||
os_info = { version = "3.15.0", default-features = false }
|
os_info = { version = "3.15.0", default-features = false }
|
||||||
reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "stream"] }
|
reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "stream"] }
|
||||||
roxmltree = "0.21.1"
|
roxmltree = "0.21.1"
|
||||||
|
serde_json = "1.0.143"
|
||||||
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
|
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
|
||||||
|
vorbis_rs = "0.5.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.47.1", features = ["macros", "net", "rt-multi-thread", "sync", "test-util", "time"] }
|
tokio = { version = "1.47.1", features = ["macros", "net", "rt-multi-thread", "sync", "test-util", "time"] }
|
||||||
|
|||||||
484
crates/libremetaverse/src/asset_cache.rs
Normal file
484
crates/libremetaverse/src/asset_cache.rs
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
//! Cross-platform, bounded on-disk asset cache.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_sign_loss,
|
||||||
|
clippy::collapsible_if,
|
||||||
|
clippy::missing_errors_doc,
|
||||||
|
clippy::must_use_candidate,
|
||||||
|
clippy::needless_pass_by_value,
|
||||||
|
clippy::single_match_else,
|
||||||
|
clippy::type_complexity,
|
||||||
|
clippy::uninlined_format_args,
|
||||||
|
clippy::unused_async,
|
||||||
|
clippy::unused_self
|
||||||
|
)]
|
||||||
|
|
||||||
|
use crate::{Error, GridClient, ImageDownload};
|
||||||
|
use libremetaverse_types::UUID;
|
||||||
|
use libremetaverse_types::compat::{CancellationToken, Object};
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const CACHE_TARGET_PERCENT: i64 = 90;
|
||||||
|
const ABSOLUTE_CACHE_FILE_LIMIT: u64 = crate::asset_models::MAX_ASSET_BYTES as u64;
|
||||||
|
|
||||||
|
type CacheFilenameHandler = dyn Fn(String, UUID) -> Result<String, Error> + Send + Sync;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AssetCacheComputeAssetCacheFilenameDelegate {
|
||||||
|
handler: Arc<CacheFilenameHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for AssetCacheComputeAssetCacheFilenameDelegate {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("AssetCacheComputeAssetCacheFilenameDelegate")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetCacheComputeAssetCacheFilenameDelegate {
|
||||||
|
pub fn from_handler(
|
||||||
|
handler: impl Fn(String, UUID) -> Result<String, Error> + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
handler: Arc::new(handler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
|
||||||
|
if method == 0 {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
object
|
||||||
|
.downcast_arc::<Self>()
|
||||||
|
.map(|value| (*value).clone())
|
||||||
|
.ok_or(Error::Argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invoke(&self, cache_dir: String, asset_id: UUID) -> Result<String, Error> {
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
(self.handler)(cache_dir, asset_id)
|
||||||
|
}))
|
||||||
|
.map_err(|_| Error::InvalidOperation)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_invoke(
|
||||||
|
&self,
|
||||||
|
cache_dir: String,
|
||||||
|
asset_id: UUID,
|
||||||
|
callback: Box<dyn Fn(&dyn std::any::Any) + Send + Sync>,
|
||||||
|
object: Object,
|
||||||
|
) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
|
||||||
|
let result = self.invoke(cache_dir, asset_id)?;
|
||||||
|
callback(&object);
|
||||||
|
Ok(Box::new(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_invoke(
|
||||||
|
&self,
|
||||||
|
result: Box<dyn std::any::Any + Send + Sync>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
result
|
||||||
|
.downcast::<String>()
|
||||||
|
.map(|value| *value)
|
||||||
|
.map_err(|_| Error::Argument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssetCacheInner {
|
||||||
|
client: crate::client_core::ClientWeakHandle,
|
||||||
|
pruning: AtomicBool,
|
||||||
|
auto_prune: AtomicBool,
|
||||||
|
interval_bits: AtomicU64,
|
||||||
|
last_prune_ms: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssetCache {
|
||||||
|
inner: Arc<AssetCacheInner>,
|
||||||
|
pub compute_asset_cache_filename: Option<AssetCacheComputeAssetCacheFilenameDelegate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for AssetCache {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::clone(&self.inner),
|
||||||
|
compute_asset_cache_filename: self.compute_asset_cache_filename.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for AssetCache {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("AssetCache")
|
||||||
|
.field("auto_prune_enabled", &self.auto_prune_enabled())
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetCache {
|
||||||
|
fn now_ms() -> u64 {
|
||||||
|
u64::try_from(
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis(),
|
||||||
|
)
|
||||||
|
.unwrap_or(u64::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(client: GridClient) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: Arc::new(AssetCacheInner {
|
||||||
|
client: client.native_weak_handle(),
|
||||||
|
pruning: AtomicBool::new(false),
|
||||||
|
auto_prune: AtomicBool::new(true),
|
||||||
|
interval_bits: AtomicU64::new((300_000_f64).to_bits()),
|
||||||
|
last_prune_ms: AtomicU64::new(Self::now_ms()),
|
||||||
|
}),
|
||||||
|
compute_asset_cache_filename: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config(&self) -> Result<crate::AssetCacheSettings, Error> {
|
||||||
|
let client = self.inner.client.upgrade().ok_or(Error::InvalidOperation)?;
|
||||||
|
Ok(client.settings_ref().asset_cache())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_dir(&self) -> Result<PathBuf, Error> {
|
||||||
|
let config = self.config()?;
|
||||||
|
let dir = PathBuf::from(config.dir);
|
||||||
|
if dir.as_os_str().is_empty() {
|
||||||
|
return Err(Error::InvalidOperation);
|
||||||
|
}
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self, id: UUID) -> Result<PathBuf, Error> {
|
||||||
|
let dir = self.cache_dir()?;
|
||||||
|
if let Some(compute) = &self.compute_asset_cache_filename {
|
||||||
|
let path = PathBuf::from(compute.invoke(dir.to_string_lossy().into_owned(), id)?);
|
||||||
|
if path.parent() == Some(dir.as_path()) {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
Ok(dir.join(id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operational(&self) -> Result<bool, Error> {
|
||||||
|
Ok(self.config()?.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_read(&self, path: &Path) -> Result<Option<Vec<u8>>, Error> {
|
||||||
|
let metadata = match fs::metadata(path) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(_) => return Err(Error::InvalidOperation),
|
||||||
|
};
|
||||||
|
if !metadata.is_file() || metadata.len() == 0 || metadata.len() > ABSOLUTE_CACHE_FILE_LIMIT
|
||||||
|
{
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let bytes = fs::read(path).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
if bytes.len() as u64 != metadata.len() {
|
||||||
|
return Err(Error::InvalidOperation);
|
||||||
|
}
|
||||||
|
Ok(Some(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cached_asset_bytes_with_uuid(
|
||||||
|
&self,
|
||||||
|
asset_id: UUID,
|
||||||
|
) -> Result<Option<Vec<u8>>, Error> {
|
||||||
|
if !self.operational()? {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
self.checked_read(&self.path(asset_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_cached_asset_bytes_with_uuid_cancellation_token(
|
||||||
|
&self,
|
||||||
|
asset_id: UUID,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<Option<Vec<u8>>, Error> {
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
let result = self.get_cached_asset_bytes_with_uuid(asset_id);
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_get_cached_asset_bytes_with_uuid_bytes(
|
||||||
|
&self,
|
||||||
|
asset_id: UUID,
|
||||||
|
data: &mut Option<Vec<u8>>,
|
||||||
|
) -> bool {
|
||||||
|
match self.get_cached_asset_bytes_with_uuid(asset_id) {
|
||||||
|
Ok(Some(bytes)) => {
|
||||||
|
*data = Some(bytes);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
*data = None;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn try_get_cached_asset_bytes_with_uuid_cancellation_token(
|
||||||
|
&self,
|
||||||
|
asset_id: UUID,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<(bool, Option<Vec<u8>>), Error> {
|
||||||
|
let data = self
|
||||||
|
.get_cached_asset_bytes_with_uuid_cancellation_token(asset_id, cancellation_token)
|
||||||
|
.await?;
|
||||||
|
Ok((data.is_some(), data))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_asset_to_cache_with_uuid_bytes(
|
||||||
|
&self,
|
||||||
|
asset_id: UUID,
|
||||||
|
asset_data: Vec<u8>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
if !self.operational()? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if asset_data.is_empty() || asset_data.len() > crate::asset_models::MAX_ASSET_BYTES {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let path = self.path(asset_id)?;
|
||||||
|
if path.is_file() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
let dir = path.parent().ok_or(Error::Argument)?;
|
||||||
|
fs::create_dir_all(dir).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
let nonce = UUID::random()?.to_string();
|
||||||
|
let temp = dir.join(format!(".{}.{}.tmp", asset_id, nonce));
|
||||||
|
let write_result = (|| -> Result<(), Error> {
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(&temp)
|
||||||
|
.map_err(|_| Error::InvalidOperation)?;
|
||||||
|
file.write_all(&asset_data)
|
||||||
|
.map_err(|_| Error::InvalidOperation)?;
|
||||||
|
file.sync_all().map_err(|_| Error::InvalidOperation)?;
|
||||||
|
match fs::rename(&temp, &path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(_) if path.is_file() => Ok(()),
|
||||||
|
Err(_) => Err(Error::InvalidOperation),
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
if write_result.is_err() || temp.exists() {
|
||||||
|
let _ = fs::remove_file(&temp);
|
||||||
|
}
|
||||||
|
write_result.map(|()| {
|
||||||
|
self.maybe_begin_prune();
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_begin_prune(&self) {
|
||||||
|
if !self.auto_prune_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let interval = self.auto_prune_interval();
|
||||||
|
if !interval.is_finite() || interval <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let now = Self::now_ms();
|
||||||
|
let last = self.inner.last_prune_ms.load(Ordering::Acquire);
|
||||||
|
if now.saturating_sub(last) < interval as u64 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.inner
|
||||||
|
.last_prune_ms
|
||||||
|
.compare_exchange(last, now, Ordering::AcqRel, Ordering::Acquire)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let _ = self.begin_prune();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_asset_to_cache_with_uuid_bytes_cancellation_token(
|
||||||
|
&self,
|
||||||
|
asset_id: UUID,
|
||||||
|
asset_data: Vec<u8>,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
let result = self.save_asset_to_cache_with_uuid_bytes(asset_id, asset_data);
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asset_file_name(&self, asset_id: UUID) -> Result<Option<String>, Error> {
|
||||||
|
if !self.operational()? {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let path = self.path(asset_id)?;
|
||||||
|
Ok(path.is_file().then(|| path.to_string_lossy().into_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_asset(&self, asset_id: UUID) -> Result<bool, Error> {
|
||||||
|
Ok(self.operational()? && self.path(asset_id)?.is_file())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_entries(&self) -> Result<Vec<(PathBuf, u64, SystemTime)>, Error> {
|
||||||
|
let dir = self.cache_dir()?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let read = match fs::read_dir(dir) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(entries),
|
||||||
|
Err(_) => return Err(Error::InvalidOperation),
|
||||||
|
};
|
||||||
|
for entry in read.flatten() {
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
if metadata.is_file() {
|
||||||
|
entries.push((
|
||||||
|
entry.path(),
|
||||||
|
metadata.len(),
|
||||||
|
metadata
|
||||||
|
.accessed()
|
||||||
|
.or_else(|_| metadata.modified())
|
||||||
|
.unwrap_or(UNIX_EPOCH),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&self) -> Result<(), Error> {
|
||||||
|
for (path, _, _) in self.cache_entries()? {
|
||||||
|
fs::remove_file(path).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prune(&self, cancellation_token: Option<CancellationToken>) -> Result<(), Error> {
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
let max = self.config()?.max_size;
|
||||||
|
if max <= 0 {
|
||||||
|
return Err(Error::InvalidOperation);
|
||||||
|
}
|
||||||
|
let mut entries = self.cache_entries()?;
|
||||||
|
let mut size: u64 = entries.iter().map(|(_, size, _)| *size).sum();
|
||||||
|
let max = u64::try_from(max).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
if size <= max {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
entries.sort_unstable_by_key(|(_, _, accessed)| *accessed);
|
||||||
|
let target = max.saturating_mul(CACHE_TARGET_PERCENT as u64) / 100;
|
||||||
|
for (path, length, _) in entries {
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
fs::remove_file(path).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
size = size.saturating_sub(length);
|
||||||
|
if size <= target {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_prune(&self) -> Result<(), Error> {
|
||||||
|
if self.inner.pruning.swap(true, Ordering::AcqRel) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let cache = self.clone();
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("asset-cache-prune".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let _ = cache.prune_sync(None);
|
||||||
|
cache.inner.pruning.store(false, Ordering::Release);
|
||||||
|
})
|
||||||
|
.map_err(|_| {
|
||||||
|
self.inner.pruning.store(false, Ordering::Release);
|
||||||
|
Error::InvalidOperation
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune_sync(&self, cancellation_token: Option<CancellationToken>) -> Result<(), Error> {
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
let max = self.config()?.max_size;
|
||||||
|
if max <= 0 {
|
||||||
|
return Err(Error::InvalidOperation);
|
||||||
|
}
|
||||||
|
let mut entries = self.cache_entries()?;
|
||||||
|
let mut size: u64 = entries.iter().map(|(_, size, _)| *size).sum();
|
||||||
|
let max = u64::try_from(max).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
if size <= max {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
entries.sort_unstable_by_key(|(_, _, accessed)| *accessed);
|
||||||
|
let target = max.saturating_mul(CACHE_TARGET_PERCENT as u64) / 100;
|
||||||
|
for (path, length, _) in entries {
|
||||||
|
if let Some(token) = &cancellation_token {
|
||||||
|
token.throw_if_cancellation_requested()?;
|
||||||
|
}
|
||||||
|
fs::remove_file(path).map_err(|_| Error::InvalidOperation)?;
|
||||||
|
size = size.saturating_sub(length);
|
||||||
|
if size <= target {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispose(&self) -> Result<(), Error> {
|
||||||
|
self.inner.auto_prune.store(false, Ordering::Release);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn get_cached_image(&self, image_id: UUID) -> Result<Option<ImageDownload>, Error> {
|
||||||
|
let Some(bytes) = self.get_cached_asset_bytes_with_uuid(image_id)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let mut image = ImageDownload::new()?;
|
||||||
|
image.base.id = image_id;
|
||||||
|
image.base.asset_type = libremetaverse_types::AssetType::Texture;
|
||||||
|
image.base.size = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?;
|
||||||
|
image.base.transferred = image.base.size;
|
||||||
|
image.base.success = true;
|
||||||
|
image.base.asset_data = bytes;
|
||||||
|
image.codec = crate::ImageCodec::J2C;
|
||||||
|
Ok(Some(image))
|
||||||
|
}
|
||||||
|
pub fn auto_prune_enabled(&self) -> bool {
|
||||||
|
self.inner.auto_prune.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
pub fn set_auto_prune_enabled(&mut self, value: bool) {
|
||||||
|
self.inner.auto_prune.store(value, Ordering::Release);
|
||||||
|
}
|
||||||
|
pub fn auto_prune_interval(&self) -> f64 {
|
||||||
|
f64::from_bits(self.inner.interval_bits.load(Ordering::Acquire))
|
||||||
|
}
|
||||||
|
pub fn set_auto_prune_interval(&mut self, value: f64) {
|
||||||
|
if value.is_finite() && value > 0.0 {
|
||||||
|
self.inner
|
||||||
|
.interval_bits
|
||||||
|
.store(value.to_bits(), Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1319
crates/libremetaverse/src/asset_manager.rs
Normal file
1319
crates/libremetaverse/src/asset_manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
719
crates/libremetaverse/src/asset_material.rs
Normal file
719
crates/libremetaverse/src/asset_material.rs
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
//! GLTF material asset and simulator material-override encoding.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
clippy::assigning_clones,
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::collapsible_if,
|
||||||
|
clippy::float_cmp,
|
||||||
|
clippy::missing_errors_doc,
|
||||||
|
clippy::missing_panics_doc,
|
||||||
|
clippy::must_use_candidate,
|
||||||
|
clippy::needless_pass_by_value,
|
||||||
|
clippy::should_implement_trait
|
||||||
|
)]
|
||||||
|
|
||||||
|
use crate::{Error, assets::GltfAlphaMode};
|
||||||
|
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||||
|
use libremetaverse_types::{AssetType, Color4, UUID, Vector2, Vector3};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
const FLOAT_EPSILON: f32 = 1.192_092_9e-7;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct GltfTextureTransform {
|
||||||
|
pub offset: Vector2,
|
||||||
|
pub rotation: f32,
|
||||||
|
pub scale: Vector2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GltfTextureTransform {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
offset: Vector2::zero(),
|
||||||
|
rotation: 0.0,
|
||||||
|
scale: Vector2 { x: 1.0, y: 1.0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn equals_with_gltf_texture_transform(&self, other: Self) -> bool {
|
||||||
|
*self == other
|
||||||
|
}
|
||||||
|
pub fn equals_with_object(&self, _obj: Option<libremetaverse_types::compat::Object>) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
pub fn get_hash_code(&self) -> i32 {
|
||||||
|
(self.offset.x.to_bits()
|
||||||
|
^ self.offset.y.to_bits()
|
||||||
|
^ self.rotation.to_bits()
|
||||||
|
^ self.scale.x.to_bits()
|
||||||
|
^ self.scale.y.to_bits())
|
||||||
|
.cast_signed()
|
||||||
|
}
|
||||||
|
pub fn is_default(&self) -> bool {
|
||||||
|
self.offset.x.abs() <= FLOAT_EPSILON
|
||||||
|
&& self.offset.y.abs() <= FLOAT_EPSILON
|
||||||
|
&& self.rotation.abs() <= FLOAT_EPSILON
|
||||||
|
&& (self.scale.x - 1.0).abs() <= FLOAT_EPSILON
|
||||||
|
&& (self.scale.y - 1.0).abs() <= FLOAT_EPSILON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetMaterial {
|
||||||
|
asset_id: UUID,
|
||||||
|
asset_data: Vec<u8>,
|
||||||
|
name: String,
|
||||||
|
texture_ids: Vec<UUID>,
|
||||||
|
texture_transforms: Vec<GltfTextureTransform>,
|
||||||
|
base_color_factor: Color4,
|
||||||
|
emissive_factor: Vector3,
|
||||||
|
metallic_factor: f32,
|
||||||
|
roughness_factor: f32,
|
||||||
|
alpha_cutoff: f32,
|
||||||
|
alpha_mode: GltfAlphaMode,
|
||||||
|
double_sided: bool,
|
||||||
|
override_alpha_mode: bool,
|
||||||
|
override_double_sided: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetMaterial {
|
||||||
|
pub const TEXTURE_BASE_COLOR: i32 = 0;
|
||||||
|
pub const TEXTURE_NORMAL: i32 = 1;
|
||||||
|
pub const TEXTURE_METALLIC_ROUGHNESS: i32 = 2;
|
||||||
|
pub const TEXTURE_EMISSIVE: i32 = 3;
|
||||||
|
pub const TEXTURE_COUNT: i32 = 4;
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
asset_id: UUID::zero(),
|
||||||
|
asset_data: Vec::new(),
|
||||||
|
name: String::new(),
|
||||||
|
texture_ids: vec![UUID::zero(); Self::TEXTURE_COUNT as usize],
|
||||||
|
texture_transforms: vec![GltfTextureTransform::default(); Self::TEXTURE_COUNT as usize],
|
||||||
|
base_color_factor: Color4 {
|
||||||
|
r: 1.0,
|
||||||
|
g: 1.0,
|
||||||
|
b: 1.0,
|
||||||
|
a: 1.0,
|
||||||
|
},
|
||||||
|
emissive_factor: Vector3::zero(),
|
||||||
|
metallic_factor: 1.0,
|
||||||
|
roughness_factor: 1.0,
|
||||||
|
alpha_cutoff: 0.5,
|
||||||
|
alpha_mode: GltfAlphaMode::Opaque,
|
||||||
|
double_sided: false,
|
||||||
|
override_alpha_mode: false,
|
||||||
|
override_double_sided: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gltf_override_null_uuid() -> UUID {
|
||||||
|
UUID::new_with_string("ffffffff-ffff-ffff-ffff-ffffffffffff".into())
|
||||||
|
.expect("constant UUID is valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
if asset_data.len() > crate::asset_models::MAX_ASSET_BYTES {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let mut material = Self {
|
||||||
|
asset_id,
|
||||||
|
asset_data,
|
||||||
|
..Self::default()
|
||||||
|
};
|
||||||
|
if !material.decode()? {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
Ok(material)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slot(slot: i32) -> Result<usize, Error> {
|
||||||
|
usize::try_from(slot)
|
||||||
|
.ok()
|
||||||
|
.filter(|slot| *slot < Self::TEXTURE_COUNT as usize)
|
||||||
|
.ok_or(Error::Argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_texture_id(
|
||||||
|
&mut self,
|
||||||
|
slot: i32,
|
||||||
|
id: UUID,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let slot = Self::slot(slot)?;
|
||||||
|
self.texture_ids[slot] = if for_override.unwrap_or(false) && id == UUID::zero() {
|
||||||
|
Self::gltf_override_null_uuid()
|
||||||
|
} else {
|
||||||
|
id
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_texture_offset(&mut self, slot: i32, offset: Vector2) -> Result<(), Error> {
|
||||||
|
self.texture_transforms[Self::slot(slot)?].offset = offset;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_texture_scale(&mut self, slot: i32, scale: Vector2) -> Result<(), Error> {
|
||||||
|
self.texture_transforms[Self::slot(slot)?].scale = scale;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_texture_rotation(&mut self, slot: i32, rotation: f32) -> Result<(), Error> {
|
||||||
|
self.texture_transforms[Self::slot(slot)?].rotation = rotation;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_base_color_factor_with_color4_boolean(
|
||||||
|
&mut self,
|
||||||
|
mut color: Color4,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if for_override.unwrap_or(false) && color == Self::default().base_color_factor {
|
||||||
|
color.a -= FLOAT_EPSILON;
|
||||||
|
}
|
||||||
|
self.base_color_factor = color;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_emissive_factor_with_vector3_boolean(
|
||||||
|
&mut self,
|
||||||
|
mut color: Vector3,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if for_override.unwrap_or(false) && color == Vector3::zero() {
|
||||||
|
color.x += FLOAT_EPSILON;
|
||||||
|
}
|
||||||
|
self.emissive_factor = color;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_metallic_factor_with_single_boolean(
|
||||||
|
&mut self,
|
||||||
|
metallic: f32,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !metallic.is_finite() {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
self.metallic_factor = if for_override.unwrap_or(false) {
|
||||||
|
metallic.min(1.0 - FLOAT_EPSILON)
|
||||||
|
} else {
|
||||||
|
metallic
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_roughness_factor_with_single_boolean(
|
||||||
|
&mut self,
|
||||||
|
roughness: f32,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !roughness.is_finite() {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
self.roughness_factor = if for_override.unwrap_or(false) {
|
||||||
|
roughness.min(1.0 - FLOAT_EPSILON)
|
||||||
|
} else {
|
||||||
|
roughness
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_alpha_cutoff_with_single_boolean(
|
||||||
|
&mut self,
|
||||||
|
cutoff: f32,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !cutoff.is_finite() {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
self.alpha_cutoff = if for_override.unwrap_or(false) && cutoff == 0.5 {
|
||||||
|
cutoff - FLOAT_EPSILON
|
||||||
|
} else {
|
||||||
|
cutoff
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_alpha_mode_with_gltf_alpha_mode_boolean(
|
||||||
|
&mut self,
|
||||||
|
mode: GltfAlphaMode,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.alpha_mode = mode;
|
||||||
|
self.override_alpha_mode = for_override.unwrap_or(false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn set_double_sided_with_boolean_boolean(
|
||||||
|
&mut self,
|
||||||
|
double_sided: bool,
|
||||||
|
for_override: Option<bool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.double_sided = double_sided;
|
||||||
|
self.override_double_sided = for_override.unwrap_or(false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_base_material(&mut self) -> Result<(), Error> {
|
||||||
|
let transforms = self.texture_transforms.clone();
|
||||||
|
let asset_id = self.asset_id;
|
||||||
|
*self = Self::default();
|
||||||
|
self.asset_id = asset_id;
|
||||||
|
self.texture_transforms = transforms;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_override(&mut self, value: Self) -> Result<(), Error> {
|
||||||
|
let sentinel = Self::gltf_override_null_uuid();
|
||||||
|
for slot in 0..Self::TEXTURE_COUNT as usize {
|
||||||
|
if value.texture_ids[slot] == sentinel {
|
||||||
|
self.texture_ids[slot] = UUID::zero();
|
||||||
|
} else if value.texture_ids[slot] != UUID::zero() {
|
||||||
|
self.texture_ids[slot] = value.texture_ids[slot];
|
||||||
|
}
|
||||||
|
if !value.texture_transforms[slot].is_default() {
|
||||||
|
self.texture_transforms[slot] = value.texture_transforms[slot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let defaults = Self::default();
|
||||||
|
if value.base_color_factor != defaults.base_color_factor {
|
||||||
|
self.base_color_factor = value.base_color_factor;
|
||||||
|
}
|
||||||
|
if value.emissive_factor != defaults.emissive_factor {
|
||||||
|
self.emissive_factor = value.emissive_factor;
|
||||||
|
}
|
||||||
|
if value.metallic_factor != defaults.metallic_factor {
|
||||||
|
self.metallic_factor = value.metallic_factor;
|
||||||
|
}
|
||||||
|
if value.roughness_factor != defaults.roughness_factor {
|
||||||
|
self.roughness_factor = value.roughness_factor;
|
||||||
|
}
|
||||||
|
if value.alpha_cutoff != defaults.alpha_cutoff {
|
||||||
|
self.alpha_cutoff = value.alpha_cutoff;
|
||||||
|
}
|
||||||
|
if value.override_alpha_mode {
|
||||||
|
self.alpha_mode = value.alpha_mode;
|
||||||
|
}
|
||||||
|
if value.override_double_sided {
|
||||||
|
self.double_sided = value.double_sided;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha_name(mode: GltfAlphaMode) -> &'static str {
|
||||||
|
match mode {
|
||||||
|
GltfAlphaMode::Opaque => "OPAQUE",
|
||||||
|
GltfAlphaMode::Blend => "BLEND",
|
||||||
|
GltfAlphaMode::Mask => "MASK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn parse_alpha(value: Option<&str>) -> Result<GltfAlphaMode, Error> {
|
||||||
|
match value.unwrap_or("OPAQUE") {
|
||||||
|
"OPAQUE" => Ok(GltfAlphaMode::Opaque),
|
||||||
|
"BLEND" => Ok(GltfAlphaMode::Blend),
|
||||||
|
"MASK" => Ok(GltfAlphaMode::Mask),
|
||||||
|
_ => Err(Error::Argument),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_json(
|
||||||
|
&self,
|
||||||
|
slot: usize,
|
||||||
|
images: &mut Vec<Value>,
|
||||||
|
textures: &mut Vec<Value>,
|
||||||
|
) -> Option<Value> {
|
||||||
|
let id = self.texture_ids[slot];
|
||||||
|
if id == UUID::zero() || id == Self::gltf_override_null_uuid() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let index = images.len();
|
||||||
|
images.push(json!({"uri": id.to_string()}));
|
||||||
|
textures.push(json!({"source": index}));
|
||||||
|
let mut info = json!({"index": index});
|
||||||
|
let transform = self.texture_transforms[slot];
|
||||||
|
if !transform.is_default() {
|
||||||
|
info["extensions"] = json!({"KHR_texture_transform": {"offset": [transform.offset.x, transform.offset.y], "scale": [transform.scale.x, transform.scale.y], "rotation": transform.rotation}});
|
||||||
|
}
|
||||||
|
Some(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> Result<String, Error> {
|
||||||
|
let mut images = Vec::new();
|
||||||
|
let mut textures = Vec::new();
|
||||||
|
let base = self.texture_json(
|
||||||
|
Self::TEXTURE_BASE_COLOR as usize,
|
||||||
|
&mut images,
|
||||||
|
&mut textures,
|
||||||
|
);
|
||||||
|
let normal = self.texture_json(Self::TEXTURE_NORMAL as usize, &mut images, &mut textures);
|
||||||
|
let metallic = self.texture_json(
|
||||||
|
Self::TEXTURE_METALLIC_ROUGHNESS as usize,
|
||||||
|
&mut images,
|
||||||
|
&mut textures,
|
||||||
|
);
|
||||||
|
let emissive =
|
||||||
|
self.texture_json(Self::TEXTURE_EMISSIVE as usize, &mut images, &mut textures);
|
||||||
|
let mut pbr = json!({"baseColorFactor": [self.base_color_factor.r,self.base_color_factor.g,self.base_color_factor.b,self.base_color_factor.a], "metallicFactor": self.metallic_factor, "roughnessFactor": self.roughness_factor});
|
||||||
|
if let Some(value) = base {
|
||||||
|
pbr["baseColorTexture"] = value;
|
||||||
|
}
|
||||||
|
if let Some(value) = metallic {
|
||||||
|
pbr["metallicRoughnessTexture"] = value;
|
||||||
|
}
|
||||||
|
let mut material = json!({"name": self.name, "pbrMetallicRoughness": pbr, "emissiveFactor": [self.emissive_factor.x,self.emissive_factor.y,self.emissive_factor.z], "alphaMode": Self::alpha_name(self.alpha_mode), "alphaCutoff": self.alpha_cutoff, "doubleSided": self.double_sided});
|
||||||
|
if let Some(value) = normal {
|
||||||
|
material["normalTexture"] = value;
|
||||||
|
}
|
||||||
|
if let Some(value) = emissive {
|
||||||
|
material["emissiveTexture"] = value;
|
||||||
|
}
|
||||||
|
let mut root = json!({"asset":{"version":"2.0"},"materials":[material]});
|
||||||
|
if !images.is_empty() {
|
||||||
|
root["images"] = Value::Array(images);
|
||||||
|
root["textures"] = Value::Array(textures);
|
||||||
|
root["extensionsUsed"] = json!(["KHR_texture_transform"]);
|
||||||
|
}
|
||||||
|
serde_json::to_string(&root).map_err(|_| Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&mut self) -> Result<(), Error> {
|
||||||
|
self.asset_data = self.to_json()?.into_bytes();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn decode(&mut self) -> Result<bool, Error> {
|
||||||
|
let root: Value = serde_json::from_slice(&self.asset_data).map_err(|_| Error::Argument)?;
|
||||||
|
let mat = root
|
||||||
|
.get("materials")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.and_then(|v| v.first())
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
self.name = mat
|
||||||
|
.get("name")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned();
|
||||||
|
let pbr = mat.get("pbrMetallicRoughness").and_then(Value::as_object);
|
||||||
|
if let Some(values) = pbr
|
||||||
|
.and_then(|v| v.get("baseColorFactor"))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
if values.len() == 4 {
|
||||||
|
self.base_color_factor = Color4 {
|
||||||
|
r: number(&values[0])?,
|
||||||
|
g: number(&values[1])?,
|
||||||
|
b: number(&values[2])?,
|
||||||
|
a: number(&values[3])?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.metallic_factor = pbr
|
||||||
|
.and_then(|v| v.get("metallicFactor"))
|
||||||
|
.map(number)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
self.roughness_factor = pbr
|
||||||
|
.and_then(|v| v.get("roughnessFactor"))
|
||||||
|
.map(number)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
if let Some(values) = mat.get("emissiveFactor").and_then(Value::as_array) {
|
||||||
|
if values.len() == 3 {
|
||||||
|
self.emissive_factor = Vector3 {
|
||||||
|
x: number(&values[0])?,
|
||||||
|
y: number(&values[1])?,
|
||||||
|
z: number(&values[2])?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.alpha_mode = Self::parse_alpha(mat.get("alphaMode").and_then(Value::as_str))?;
|
||||||
|
self.alpha_cutoff = mat
|
||||||
|
.get("alphaCutoff")
|
||||||
|
.map(number)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(0.5);
|
||||||
|
self.double_sided = mat
|
||||||
|
.get("doubleSided")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let textures = root.get("textures").and_then(Value::as_array);
|
||||||
|
let images = root.get("images").and_then(Value::as_array);
|
||||||
|
for (slot, info) in [
|
||||||
|
(0, pbr.and_then(|v| v.get("baseColorTexture"))),
|
||||||
|
(1, mat.get("normalTexture")),
|
||||||
|
(2, pbr.and_then(|v| v.get("metallicRoughnessTexture"))),
|
||||||
|
(3, mat.get("emissiveTexture")),
|
||||||
|
] {
|
||||||
|
if let Some(info) = info {
|
||||||
|
self.decode_texture(slot, info, textures, images)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_texture(
|
||||||
|
&mut self,
|
||||||
|
slot: usize,
|
||||||
|
info: &Value,
|
||||||
|
textures: Option<&Vec<Value>>,
|
||||||
|
images: Option<&Vec<Value>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let index = info
|
||||||
|
.get("index")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.and_then(|v| usize::try_from(v).ok())
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
let source = textures
|
||||||
|
.and_then(|v| v.get(index))
|
||||||
|
.and_then(|v| v.get("source"))
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.and_then(|v| usize::try_from(v).ok())
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
let uri = images
|
||||||
|
.and_then(|v| v.get(source))
|
||||||
|
.and_then(|v| v.get("uri"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
self.texture_ids[slot] = UUID::new_with_string(uri.to_owned())?;
|
||||||
|
if let Some(transform) = info.pointer("/extensions/KHR_texture_transform") {
|
||||||
|
if let Some(v) = transform.get("offset").and_then(Value::as_array) {
|
||||||
|
if v.len() == 2 {
|
||||||
|
self.texture_transforms[slot].offset = Vector2 {
|
||||||
|
x: number(&v[0])?,
|
||||||
|
y: number(&v[1])?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = transform.get("scale").and_then(Value::as_array) {
|
||||||
|
if v.len() == 2 {
|
||||||
|
self.texture_transforms[slot].scale = Vector2 {
|
||||||
|
x: number(&v[0])?,
|
||||||
|
y: number(&v[1])?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = transform.get("rotation") {
|
||||||
|
self.texture_transforms[slot].rotation = number(v)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_override_osd(&self) -> Result<OSDMap, Error> {
|
||||||
|
let defaults = Self::default();
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
let mut tex = Vec::new();
|
||||||
|
let mut last = None;
|
||||||
|
for (index, id) in self.texture_ids.iter().enumerate() {
|
||||||
|
if *id != UUID::zero() {
|
||||||
|
last = Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(last) = last {
|
||||||
|
for id in &self.texture_ids[..=last] {
|
||||||
|
tex.push(OSD::UUID(*id));
|
||||||
|
}
|
||||||
|
map.insert("tex".into(), OSD::Array(tex));
|
||||||
|
}
|
||||||
|
if self.base_color_factor != defaults.base_color_factor {
|
||||||
|
map.insert("bc".into(), color_osd(self.base_color_factor));
|
||||||
|
}
|
||||||
|
if self.emissive_factor != defaults.emissive_factor {
|
||||||
|
map.insert("ec".into(), vector3_osd(self.emissive_factor));
|
||||||
|
}
|
||||||
|
if self.metallic_factor != defaults.metallic_factor {
|
||||||
|
map.insert("mf".into(), OSD::Real(f64::from(self.metallic_factor)));
|
||||||
|
}
|
||||||
|
if self.roughness_factor != defaults.roughness_factor {
|
||||||
|
map.insert("rf".into(), OSD::Real(f64::from(self.roughness_factor)));
|
||||||
|
}
|
||||||
|
if self.override_alpha_mode {
|
||||||
|
map.insert("am".into(), OSD::Integer(self.alpha_mode as i32));
|
||||||
|
}
|
||||||
|
if self.alpha_cutoff != defaults.alpha_cutoff {
|
||||||
|
map.insert("ac".into(), OSD::Real(f64::from(self.alpha_cutoff)));
|
||||||
|
}
|
||||||
|
if self.override_double_sided {
|
||||||
|
map.insert("ds".into(), OSD::Boolean(self.double_sided));
|
||||||
|
}
|
||||||
|
let transforms: Vec<_> = self
|
||||||
|
.texture_transforms
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
|
OSD::Map(HashMap::from([
|
||||||
|
("o".into(), vector2_osd(t.offset)),
|
||||||
|
("s".into(), vector2_osd(t.scale)),
|
||||||
|
("r".into(), OSD::Real(f64::from(t.rotation))),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if self.texture_transforms.iter().any(|v| !v.is_default()) {
|
||||||
|
map.insert("ti".into(), OSD::Array(transforms));
|
||||||
|
}
|
||||||
|
OSDMap::new_with_dictionary(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_override_osd(data: OSDMap) -> Result<Self, Error> {
|
||||||
|
let map = data.snapshot();
|
||||||
|
let mut value = Self::default();
|
||||||
|
if let Some(OSD::Array(ids)) = map.get("tex") {
|
||||||
|
for (slot, id) in ids.iter().take(Self::TEXTURE_COUNT as usize).enumerate() {
|
||||||
|
value.texture_ids[slot] = id.as_uuid()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("bc") {
|
||||||
|
value.base_color_factor = v.as_color4()?;
|
||||||
|
if value.base_color_factor == Self::default().base_color_factor {
|
||||||
|
value.base_color_factor.a -= FLOAT_EPSILON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("ec") {
|
||||||
|
value.emissive_factor = v.as_vector3()?;
|
||||||
|
if value.emissive_factor == Vector3::zero() {
|
||||||
|
value.emissive_factor.x += FLOAT_EPSILON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("mf") {
|
||||||
|
value.metallic_factor = (v.as_real()? as f32).min(1.0 - FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("rf") {
|
||||||
|
value.roughness_factor = (v.as_real()? as f32).min(1.0 - FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("am") {
|
||||||
|
value.alpha_mode = match v.as_integer()? {
|
||||||
|
0 => GltfAlphaMode::Opaque,
|
||||||
|
1 => GltfAlphaMode::Blend,
|
||||||
|
2 => GltfAlphaMode::Mask,
|
||||||
|
_ => return Err(Error::Argument),
|
||||||
|
};
|
||||||
|
value.override_alpha_mode = true;
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("ac") {
|
||||||
|
value.alpha_cutoff = v.as_real()? as f32;
|
||||||
|
if value.alpha_cutoff == 0.5 {
|
||||||
|
value.alpha_cutoff -= FLOAT_EPSILON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = map.get("ds") {
|
||||||
|
value.double_sided = v.as_boolean()?;
|
||||||
|
value.override_double_sided = true;
|
||||||
|
}
|
||||||
|
if let Some(OSD::Array(values)) = map.get("ti") {
|
||||||
|
for (slot, transform) in values.iter().take(Self::TEXTURE_COUNT as usize).enumerate() {
|
||||||
|
if let OSD::Map(t) = transform {
|
||||||
|
if let Some(v) = t.get("o") {
|
||||||
|
value.texture_transforms[slot].offset = v.as_vector2()?;
|
||||||
|
}
|
||||||
|
if let Some(v) = t.get("s") {
|
||||||
|
value.texture_transforms[slot].scale = v.as_vector2()?;
|
||||||
|
}
|
||||||
|
if let Some(v) = t.get("r") {
|
||||||
|
value.texture_transforms[slot].rotation = v.as_real()? as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Material
|
||||||
|
}
|
||||||
|
pub fn alpha_cutoff(&self) -> f32 {
|
||||||
|
self.alpha_cutoff
|
||||||
|
}
|
||||||
|
pub fn set_alpha_cutoff(&mut self, value: f32) {
|
||||||
|
self.alpha_cutoff = value;
|
||||||
|
}
|
||||||
|
pub fn alpha_mode(&self) -> GltfAlphaMode {
|
||||||
|
self.alpha_mode
|
||||||
|
}
|
||||||
|
pub fn set_alpha_mode(&mut self, value: GltfAlphaMode) {
|
||||||
|
self.alpha_mode = value;
|
||||||
|
}
|
||||||
|
pub fn base_color_factor(&self) -> Color4 {
|
||||||
|
self.base_color_factor
|
||||||
|
}
|
||||||
|
pub fn set_base_color_factor(&mut self, value: Color4) {
|
||||||
|
self.base_color_factor = value;
|
||||||
|
}
|
||||||
|
pub fn double_sided(&self) -> bool {
|
||||||
|
self.double_sided
|
||||||
|
}
|
||||||
|
pub fn set_double_sided(&mut self, value: bool) {
|
||||||
|
self.double_sided = value;
|
||||||
|
}
|
||||||
|
pub fn emissive_factor(&self) -> Vector3 {
|
||||||
|
self.emissive_factor
|
||||||
|
}
|
||||||
|
pub fn set_emissive_factor(&mut self, value: Vector3) {
|
||||||
|
self.emissive_factor = value;
|
||||||
|
}
|
||||||
|
pub fn metallic_factor(&self) -> f32 {
|
||||||
|
self.metallic_factor
|
||||||
|
}
|
||||||
|
pub fn set_metallic_factor(&mut self, value: f32) {
|
||||||
|
self.metallic_factor = value;
|
||||||
|
}
|
||||||
|
pub fn roughness_factor(&self) -> f32 {
|
||||||
|
self.roughness_factor
|
||||||
|
}
|
||||||
|
pub fn set_roughness_factor(&mut self, value: f32) {
|
||||||
|
self.roughness_factor = value;
|
||||||
|
}
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
pub fn set_name(&mut self, value: String) {
|
||||||
|
self.name = value;
|
||||||
|
}
|
||||||
|
pub fn override_alpha_mode(&self) -> bool {
|
||||||
|
self.override_alpha_mode
|
||||||
|
}
|
||||||
|
pub fn set_override_alpha_mode(&mut self, value: bool) {
|
||||||
|
self.override_alpha_mode = value;
|
||||||
|
}
|
||||||
|
pub fn override_double_sided(&self) -> bool {
|
||||||
|
self.override_double_sided
|
||||||
|
}
|
||||||
|
pub fn set_override_double_sided(&mut self, value: bool) {
|
||||||
|
self.override_double_sided = value;
|
||||||
|
}
|
||||||
|
pub fn texture_ids(&self) -> Vec<UUID> {
|
||||||
|
self.texture_ids.clone()
|
||||||
|
}
|
||||||
|
pub fn set_texture_ids(&mut self, value: Vec<UUID>) {
|
||||||
|
self.texture_ids = value;
|
||||||
|
}
|
||||||
|
pub fn texture_transforms(&self) -> Vec<GltfTextureTransform> {
|
||||||
|
self.texture_transforms.clone()
|
||||||
|
}
|
||||||
|
pub fn set_texture_transforms(&mut self, value: Vec<GltfTextureTransform>) {
|
||||||
|
self.texture_transforms = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number(value: &Value) -> Result<f32, Error> {
|
||||||
|
value
|
||||||
|
.as_f64()
|
||||||
|
.map(|v| v as f32)
|
||||||
|
.filter(|v| v.is_finite())
|
||||||
|
.ok_or(Error::Argument)
|
||||||
|
}
|
||||||
|
fn vector2_osd(value: Vector2) -> OSD {
|
||||||
|
OSD::Array(vec![
|
||||||
|
OSD::Real(f64::from(value.x)),
|
||||||
|
OSD::Real(f64::from(value.y)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
fn vector3_osd(value: Vector3) -> OSD {
|
||||||
|
OSD::Array(vec![
|
||||||
|
OSD::Real(f64::from(value.x)),
|
||||||
|
OSD::Real(f64::from(value.y)),
|
||||||
|
OSD::Real(f64::from(value.z)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
fn color_osd(value: Color4) -> OSD {
|
||||||
|
OSD::Array(vec![
|
||||||
|
OSD::Real(f64::from(value.r)),
|
||||||
|
OSD::Real(f64::from(value.g)),
|
||||||
|
OSD::Real(f64::from(value.b)),
|
||||||
|
OSD::Real(f64::from(value.a)),
|
||||||
|
])
|
||||||
|
}
|
||||||
676
crates/libremetaverse/src/asset_models.rs
Normal file
676
crates/libremetaverse/src/asset_models.rs
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
//! Native asset value models and their format boundaries.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
clippy::format_push_string,
|
||||||
|
clippy::missing_errors_doc,
|
||||||
|
clippy::must_use_candidate,
|
||||||
|
clippy::needless_pass_by_value,
|
||||||
|
clippy::struct_field_names
|
||||||
|
)]
|
||||||
|
|
||||||
|
use crate::{Error, Permissions};
|
||||||
|
use libremetaverse_imaging::ManagedImage;
|
||||||
|
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||||
|
use libremetaverse_types::{AssetType, SaleType, UUID, Vector3, WearableType};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
/// Maximum accepted in-memory asset payload. Network limits are enforced before
|
||||||
|
/// this boundary too, but constructors are public and must defend themselves.
|
||||||
|
pub const MAX_ASSET_BYTES: usize = 64 * 1024 * 1024;
|
||||||
|
|
||||||
|
fn validate_bytes(bytes: &[u8]) -> Result<(), Error> {
|
||||||
|
if bytes.len() > MAX_ASSET_BYTES {
|
||||||
|
Err(Error::Argument)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Asset {
|
||||||
|
pub asset_data: Vec<u8>,
|
||||||
|
pub temporary: bool,
|
||||||
|
asset_id: UUID,
|
||||||
|
asset_type: AssetType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Asset {
|
||||||
|
pub(crate) fn native_new(
|
||||||
|
asset_type: AssetType,
|
||||||
|
asset_id: UUID,
|
||||||
|
data: Vec<u8>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
validate_bytes(&data)?;
|
||||||
|
Ok(Self {
|
||||||
|
asset_data: data,
|
||||||
|
temporary: false,
|
||||||
|
asset_id,
|
||||||
|
asset_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
validate_bytes(&self.asset_data)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
validate_bytes(&self.asset_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn asset_id(&self) -> UUID {
|
||||||
|
self.asset_id
|
||||||
|
}
|
||||||
|
pub fn set_asset_id(&mut self, value: UUID) {
|
||||||
|
self.asset_id = value;
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
self.asset_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RawAsset(Arc<RwLock<Asset>>);
|
||||||
|
|
||||||
|
impl RawAsset {
|
||||||
|
fn empty(asset_type: AssetType) -> Result<Self, Error> {
|
||||||
|
Ok(Self(Arc::new(RwLock::new(Asset::native_new(
|
||||||
|
asset_type,
|
||||||
|
UUID::zero(),
|
||||||
|
Vec::new(),
|
||||||
|
)?))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_data(asset_type: AssetType, id: UUID, data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
Ok(Self(Arc::new(RwLock::new(Asset::native_new(
|
||||||
|
asset_type, id, data,
|
||||||
|
)?))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(&self) -> Result<bool, Error> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.decode()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(&self) -> Vec<u8> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.asset_data
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&self, bytes: Vec<u8>) -> Result<(), Error> {
|
||||||
|
validate_bytes(&bytes)?;
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.asset_data = bytes;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! raw_asset {
|
||||||
|
($name:ident, $kind:expr) => {
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct $name {
|
||||||
|
raw: RawAsset,
|
||||||
|
}
|
||||||
|
impl $name {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty($kind)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::with_data($kind, asset_id, asset_data)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
self.raw.decode()
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.raw.encode()
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
$kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_asset!(AssetAnimation, AssetType::Animation);
|
||||||
|
raw_asset!(AssetScriptBinary, AssetType::LSLBytecode);
|
||||||
|
raw_asset!(AssetSound, AssetType::Sound);
|
||||||
|
|
||||||
|
impl AssetSound {
|
||||||
|
pub fn pcm_to_ogg(
|
||||||
|
pcm_data: Vec<u8>,
|
||||||
|
sample_rate: i32,
|
||||||
|
channels: i32,
|
||||||
|
bits_per_sample: Option<i32>,
|
||||||
|
) -> Result<Vec<u8>, Error> {
|
||||||
|
validate_bytes(&pcm_data)?;
|
||||||
|
if pcm_data.is_empty()
|
||||||
|
|| sample_rate <= 0
|
||||||
|
|| !matches!(channels, 1 | 2)
|
||||||
|
|| !matches!(bits_per_sample.unwrap_or(16), 8 | 16)
|
||||||
|
{
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let bits = bits_per_sample.unwrap_or(16);
|
||||||
|
let bytes_per_sample = usize::try_from(bits / 8).map_err(|_| Error::Argument)?;
|
||||||
|
let channels = usize::try_from(channels).map_err(|_| Error::Argument)?;
|
||||||
|
let frame_size = bytes_per_sample
|
||||||
|
.checked_mul(channels)
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
if !pcm_data.len().is_multiple_of(frame_size) {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let frequency =
|
||||||
|
std::num::NonZeroU32::new(u32::try_from(sample_rate).map_err(|_| Error::Argument)?)
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
let channel_count =
|
||||||
|
std::num::NonZeroU8::new(u8::try_from(channels).map_err(|_| Error::Argument)?)
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
let mut builder = vorbis_rs::VorbisEncoderBuilder::new_with_serial(
|
||||||
|
frequency,
|
||||||
|
channel_count,
|
||||||
|
Vec::new(),
|
||||||
|
0x4d43_4154,
|
||||||
|
);
|
||||||
|
let mut encoder = builder.build().map_err(|_| Error::InvalidOperation)?;
|
||||||
|
let frame_count = pcm_data.len() / frame_size;
|
||||||
|
for first_frame in (0..frame_count).step_by(1024) {
|
||||||
|
let end_frame = first_frame.saturating_add(1024).min(frame_count);
|
||||||
|
let mut planar = vec![Vec::with_capacity(end_frame - first_frame); channels];
|
||||||
|
for frame in first_frame..end_frame {
|
||||||
|
for (channel, samples) in planar.iter_mut().enumerate() {
|
||||||
|
let offset = frame * frame_size + channel * bytes_per_sample;
|
||||||
|
let sample = if bits == 8 {
|
||||||
|
(f32::from(pcm_data[offset]) - 128.0) / 128.0
|
||||||
|
} else {
|
||||||
|
f32::from(i16::from_le_bytes([pcm_data[offset], pcm_data[offset + 1]]))
|
||||||
|
/ 32768.0
|
||||||
|
};
|
||||||
|
samples.push(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoder
|
||||||
|
.encode_audio_block(&planar)
|
||||||
|
.map_err(|_| Error::InvalidOperation)?;
|
||||||
|
}
|
||||||
|
let ogg_data = encoder.finish().map_err(|_| Error::InvalidOperation)?;
|
||||||
|
validate_bytes(&ogg_data)?;
|
||||||
|
Ok(ogg_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetMutable {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub current_type: AssetType,
|
||||||
|
}
|
||||||
|
impl AssetMutable {
|
||||||
|
pub fn new_with_asset_type(type_: AssetType) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(type_)?,
|
||||||
|
current_type: type_,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_asset_type_uuid_bytes(
|
||||||
|
type_: AssetType,
|
||||||
|
asset_id: UUID,
|
||||||
|
asset_data: Vec<u8>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::with_data(type_, asset_id, asset_data)?,
|
||||||
|
current_type: type_,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
self.raw.decode()
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.raw.encode()
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
self.current_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetCallingCard {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub avatar_id: UUID,
|
||||||
|
}
|
||||||
|
impl AssetCallingCard {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Self::new_with_uuid(UUID::zero())
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid(avatar_id: UUID) -> Result<Self, Error> {
|
||||||
|
let value = Self {
|
||||||
|
raw: RawAsset::empty(AssetType::CallingCard)?,
|
||||||
|
avatar_id,
|
||||||
|
};
|
||||||
|
value.encode()?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let raw = RawAsset::with_data(AssetType::CallingCard, asset_id, asset_data)?;
|
||||||
|
let text = String::from_utf8(raw.bytes()).map_err(|_| Error::Argument)?;
|
||||||
|
let id = text
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("avatar_id "))
|
||||||
|
.ok_or(Error::Argument)
|
||||||
|
.and_then(|id| UUID::new_with_string(id.trim().to_owned()))?;
|
||||||
|
Ok(Self { raw, avatar_id: id })
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
let text = String::from_utf8(self.raw.bytes()).map_err(|_| Error::Argument)?;
|
||||||
|
Ok(text.lines().any(|line| {
|
||||||
|
line.strip_prefix("avatar_id ")
|
||||||
|
.and_then(|id| UUID::new_with_string(id.trim().to_owned()).ok())
|
||||||
|
== Some(self.avatar_id)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.raw.replace(
|
||||||
|
format!(
|
||||||
|
"Linden text version 2\n{{\navatar_id {}\n}}\n",
|
||||||
|
self.avatar_id
|
||||||
|
)
|
||||||
|
.into_bytes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::CallingCard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetLandmark {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub position: Vector3,
|
||||||
|
pub region_id: UUID,
|
||||||
|
}
|
||||||
|
impl AssetLandmark {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::Landmark)?,
|
||||||
|
position: Vector3::zero(),
|
||||||
|
region_id: UUID::zero(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let raw = RawAsset::with_data(AssetType::Landmark, asset_id, asset_data)?;
|
||||||
|
let text = String::from_utf8(raw.bytes()).map_err(|_| Error::Argument)?;
|
||||||
|
let region = text
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("region_id "))
|
||||||
|
.ok_or(Error::Argument)
|
||||||
|
.and_then(|id| UUID::new_with_string(id.trim().to_owned()))?;
|
||||||
|
let values: Vec<f32> = text
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("local_pos "))
|
||||||
|
.ok_or(Error::Argument)?
|
||||||
|
.split_whitespace()
|
||||||
|
.map(str::parse)
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.map_err(|_| Error::Argument)?;
|
||||||
|
if values.len() != 3 {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
raw,
|
||||||
|
region_id: region,
|
||||||
|
position: Vector3 {
|
||||||
|
x: values[0],
|
||||||
|
y: values[1],
|
||||||
|
z: values[2],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
Ok(Self::new_with_uuid_bytes(UUID::zero(), self.raw.bytes()).is_ok())
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.raw.replace(
|
||||||
|
format!(
|
||||||
|
"Landmark version 2\nregion_id {}\nlocal_pos {} {} {}\n",
|
||||||
|
self.region_id, self.position.x, self.position.y, self.position.z
|
||||||
|
)
|
||||||
|
.into_bytes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Landmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetScriptText {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub source: Option<String>,
|
||||||
|
}
|
||||||
|
impl AssetScriptText {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::LSLText)?,
|
||||||
|
source: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let source = String::from_utf8(asset_data.clone()).map_err(|_| Error::Argument)?;
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::with_data(AssetType::LSLText, asset_id, asset_data)?,
|
||||||
|
source: Some(source),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
Ok(String::from_utf8(self.raw.bytes()).is_ok())
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.raw
|
||||||
|
.replace(self.source.clone().unwrap_or_default().into_bytes())
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::LSLText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetSettings {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub settings: Option<OSD>,
|
||||||
|
}
|
||||||
|
impl AssetSettings {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::Settings)?,
|
||||||
|
settings: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let text = String::from_utf8(asset_data.clone()).map_err(|_| Error::Argument)?;
|
||||||
|
let settings = OSDParser::deserialize_with_string(text)?;
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::with_data(AssetType::Settings, asset_id, asset_data)?,
|
||||||
|
settings: Some(settings),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
Ok(OSDParser::deserialize_with_bytes(self.raw.bytes()).is_ok())
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
let value = self.settings.clone().unwrap_or_default();
|
||||||
|
self.raw.replace(value.as_string()?.into_bytes())
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssetMesh {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub mesh_data: OSDMap,
|
||||||
|
}
|
||||||
|
impl AssetMesh {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::Mesh)?,
|
||||||
|
mesh_data: OSDMap::new_with_constructor()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let raw = RawAsset::with_data(AssetType::Mesh, asset_id, asset_data.clone())?;
|
||||||
|
let mesh_data = match OSDParser::deserialize_llsd_binary_with_bytes(asset_data)? {
|
||||||
|
OSD::Map(map) => OSDMap::new_with_dictionary(map)?,
|
||||||
|
_ => return Err(Error::Argument),
|
||||||
|
};
|
||||||
|
Ok(Self { raw, mesh_data })
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
Ok(matches!(
|
||||||
|
OSDParser::deserialize_llsd_binary_with_bytes(self.raw.bytes()),
|
||||||
|
Ok(OSD::Map(_))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.raw
|
||||||
|
.replace(OSDParser::serialize_llsd_binary_with_osd(OSD::Map(
|
||||||
|
self.mesh_data.snapshot(),
|
||||||
|
))?)
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Mesh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssetTexture {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub components: i32,
|
||||||
|
pub image: Option<ManagedImage>,
|
||||||
|
}
|
||||||
|
impl AssetTexture {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::Texture)?,
|
||||||
|
components: 0,
|
||||||
|
image: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_managed_image(image: ManagedImage) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::Texture)?,
|
||||||
|
components: 0,
|
||||||
|
image: Some(image),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::with_data(AssetType::Texture, asset_id, asset_data)?,
|
||||||
|
components: 0,
|
||||||
|
image: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
#[cfg(feature = "jpeg2000")]
|
||||||
|
{
|
||||||
|
Ok(libremetaverse_imaging::J2kCodec::decode_bytes(
|
||||||
|
&self.raw.bytes(),
|
||||||
|
libremetaverse_imaging::J2kDecodeOptions::default(),
|
||||||
|
)
|
||||||
|
.is_ok())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "jpeg2000"))]
|
||||||
|
{
|
||||||
|
validate_bytes(&self.raw.bytes())?;
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
let image = self.image.as_ref().ok_or(Error::InvalidOperation)?;
|
||||||
|
#[cfg(feature = "jpeg2000")]
|
||||||
|
{
|
||||||
|
let encoded = libremetaverse_imaging::J2kCodec::encode(
|
||||||
|
image,
|
||||||
|
libremetaverse_imaging::J2kEncodeOptions::default(),
|
||||||
|
)?;
|
||||||
|
self.raw.replace(encoded)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "jpeg2000"))]
|
||||||
|
{
|
||||||
|
let _ = image;
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Texture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssetWearable {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub creator: UUID,
|
||||||
|
pub description: String,
|
||||||
|
pub for_sale: SaleType,
|
||||||
|
pub group: UUID,
|
||||||
|
pub group_owned: bool,
|
||||||
|
pub last_owner: UUID,
|
||||||
|
pub name: String,
|
||||||
|
pub owner: UUID,
|
||||||
|
pub params: HashMap<i32, f32>,
|
||||||
|
pub permissions: Permissions,
|
||||||
|
pub sale_price: i32,
|
||||||
|
pub textures: HashMap<crate::AvatarTextureIndex, UUID>,
|
||||||
|
pub wearable_type: WearableType,
|
||||||
|
}
|
||||||
|
impl AssetWearable {
|
||||||
|
fn empty(kind: AssetType) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(kind)?,
|
||||||
|
creator: UUID::zero(),
|
||||||
|
description: String::new(),
|
||||||
|
for_sale: SaleType::Not,
|
||||||
|
group: UUID::zero(),
|
||||||
|
group_owned: false,
|
||||||
|
last_owner: UUID::zero(),
|
||||||
|
name: String::new(),
|
||||||
|
owner: UUID::zero(),
|
||||||
|
params: HashMap::new(),
|
||||||
|
permissions: Permissions::default(),
|
||||||
|
sale_price: 0,
|
||||||
|
textures: HashMap::new(),
|
||||||
|
wearable_type: WearableType::Invalid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
Ok(std::str::from_utf8(&self.raw.bytes()).is_ok())
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
let mut text = format!(
|
||||||
|
"LLWearable version 22\n{}\n{}\ntype {}\nparameters {}\n",
|
||||||
|
self.name,
|
||||||
|
self.description,
|
||||||
|
self.wearable_type as i32,
|
||||||
|
self.params.len()
|
||||||
|
);
|
||||||
|
let mut params: Vec<_> = self.params.iter().collect();
|
||||||
|
params.sort_unstable_by_key(|(id, _)| **id);
|
||||||
|
for (id, value) in params {
|
||||||
|
text.push_str(&format!("{id} {value}\n"));
|
||||||
|
}
|
||||||
|
text.push_str(&format!("textures {}\n", self.textures.len()));
|
||||||
|
self.raw.replace(text.into_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssetBodypart {
|
||||||
|
wearable: AssetWearable,
|
||||||
|
}
|
||||||
|
impl AssetBodypart {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
wearable: AssetWearable::empty(AssetType::Bodypart)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let mut wearable = AssetWearable::empty(AssetType::Bodypart)?;
|
||||||
|
wearable.raw = RawAsset::with_data(AssetType::Bodypart, asset_id, asset_data)?;
|
||||||
|
Ok(Self { wearable })
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Bodypart
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
self.wearable.decode()
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.wearable.encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct AssetClothing {
|
||||||
|
wearable: AssetWearable,
|
||||||
|
}
|
||||||
|
impl AssetClothing {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
wearable: AssetWearable::empty(AssetType::Clothing)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let mut wearable = AssetWearable::empty(AssetType::Clothing)?;
|
||||||
|
wearable.raw = RawAsset::with_data(AssetType::Clothing, asset_id, asset_data)?;
|
||||||
|
Ok(Self { wearable })
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Clothing
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
self.wearable.decode()
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
self.wearable.encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssetNotecard {
|
||||||
|
raw: RawAsset,
|
||||||
|
pub body_text: String,
|
||||||
|
pub embedded_items: Vec<crate::InventoryItem>,
|
||||||
|
}
|
||||||
|
impl AssetNotecard {
|
||||||
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::empty(AssetType::Notecard)?,
|
||||||
|
body_text: String::new(),
|
||||||
|
embedded_items: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
let text = String::from_utf8(asset_data.clone()).map_err(|_| Error::Argument)?;
|
||||||
|
let marker = "Text length ";
|
||||||
|
let start = text.find(marker).ok_or(Error::Argument)? + marker.len();
|
||||||
|
let line_end = text[start..].find('\n').ok_or(Error::Argument)? + start;
|
||||||
|
let length: usize = text[start..line_end]
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| Error::Argument)?;
|
||||||
|
let body_start = line_end + 1;
|
||||||
|
let body_end = body_start.checked_add(length).ok_or(Error::Argument)?;
|
||||||
|
let body_text = text
|
||||||
|
.get(body_start..body_end)
|
||||||
|
.ok_or(Error::Argument)?
|
||||||
|
.to_owned();
|
||||||
|
Ok(Self {
|
||||||
|
raw: RawAsset::with_data(AssetType::Notecard, asset_id, asset_data)?,
|
||||||
|
body_text,
|
||||||
|
embedded_items: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn decode(&self) -> Result<bool, Error> {
|
||||||
|
Ok(Self::new_with_uuid_bytes(UUID::zero(), self.raw.bytes()).is_ok())
|
||||||
|
}
|
||||||
|
pub fn encode(&self) -> Result<(), Error> {
|
||||||
|
if !self.embedded_items.is_empty() {
|
||||||
|
return Err(Error::InvalidOperation);
|
||||||
|
}
|
||||||
|
let bytes = self.body_text.as_bytes();
|
||||||
|
self.raw.replace(format!("Linden text version 2\n{{\nLLEmbeddedItems version 1\n{{\ncount 0\n}}\nText length {}\n{}\n}}\n", bytes.len(), self.body_text).into_bytes())
|
||||||
|
}
|
||||||
|
pub const fn asset_type(&self) -> AssetType {
|
||||||
|
AssetType::Notecard
|
||||||
|
}
|
||||||
|
}
|
||||||
133
crates/libremetaverse/src/asset_pipeline_semantics.rs
Normal file
133
crates/libremetaverse/src/asset_pipeline_semantics.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use crate::assets::{AssetMesh, AssetMutable, AssetSound, AssetTexture};
|
||||||
|
use crate::{
|
||||||
|
AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec,
|
||||||
|
};
|
||||||
|
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||||
|
use libremetaverse_types::{AssetType, UUID};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn fixture(max_size: i64) -> (GridClient, AssetCache, PathBuf) {
|
||||||
|
let path =
|
||||||
|
std::env::temp_dir().join(format!("metacrate-asset-cache-{}", UUID::random().unwrap()));
|
||||||
|
let mut client = GridClient::new().unwrap();
|
||||||
|
let settings = client.settings();
|
||||||
|
settings.asset_cache_mut().dir = path.to_string_lossy().into_owned();
|
||||||
|
settings.asset_cache_mut().enabled = true;
|
||||||
|
settings.asset_cache_mut().max_size = max_size;
|
||||||
|
let cache = AssetCache::new(client.clone()).unwrap();
|
||||||
|
(client, cache, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn asset_models_retain_type_and_reject_malformed_meshes() {
|
||||||
|
let asset = AssetMutable::new_with_asset_type_uuid_bytes(
|
||||||
|
AssetType::Sound,
|
||||||
|
UUID::random().unwrap(),
|
||||||
|
vec![1, 2, 3],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(asset.asset_type(), AssetType::Sound);
|
||||||
|
assert!(asset.decode().unwrap());
|
||||||
|
assert!(AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sound_and_texture_codecs_produce_real_valid_payloads() {
|
||||||
|
let ogg = AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)).unwrap();
|
||||||
|
assert!(ogg.starts_with(b"OggS"));
|
||||||
|
|
||||||
|
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
|
||||||
|
image.red.fill(255);
|
||||||
|
let texture = AssetTexture::new_with_managed_image(image).unwrap();
|
||||||
|
texture.encode().unwrap();
|
||||||
|
assert!(texture.decode().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_writes_are_atomic_and_corruption_is_typed() {
|
||||||
|
let (_client, cache, path) = fixture(1024);
|
||||||
|
let id = UUID::random().unwrap();
|
||||||
|
cache
|
||||||
|
.save_asset_to_cache_with_uuid_bytes(id, vec![1, 2, 3, 4])
|
||||||
|
.unwrap();
|
||||||
|
cache
|
||||||
|
.save_asset_to_cache_with_uuid_bytes(id, vec![9, 9, 9, 9])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cache.get_cached_asset_bytes_with_uuid(id).unwrap(),
|
||||||
|
Some(vec![1, 2, 3, 4])
|
||||||
|
);
|
||||||
|
|
||||||
|
let image = cache.get_cached_image(id).unwrap().unwrap();
|
||||||
|
assert_eq!(image.codec, ImageCodec::J2C);
|
||||||
|
assert_eq!(image.base.asset_data, vec![1, 2, 3, 4]);
|
||||||
|
|
||||||
|
let corrupt = UUID::random().unwrap();
|
||||||
|
fs::write(path.join(corrupt.to_string()), []).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cache.get_cached_asset_bytes_with_uuid(corrupt),
|
||||||
|
Err(Error::Argument)
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_cache_naming_is_used_but_cannot_escape_the_cache_directory() {
|
||||||
|
let (_client, mut cache, path) = fixture(1024);
|
||||||
|
cache.compute_asset_cache_filename = Some(
|
||||||
|
AssetCacheComputeAssetCacheFilenameDelegate::from_handler(|directory, id| {
|
||||||
|
Ok(PathBuf::from(directory)
|
||||||
|
.join(format!("{id}.asset"))
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let id = UUID::random().unwrap();
|
||||||
|
cache
|
||||||
|
.save_asset_to_cache_with_uuid_bytes(id, vec![7, 8, 9])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cache.get_cached_asset_bytes_with_uuid(id).unwrap(),
|
||||||
|
Some(vec![7, 8, 9])
|
||||||
|
);
|
||||||
|
assert!(path.join(format!("{id}.asset")).is_file());
|
||||||
|
cache.clear().unwrap();
|
||||||
|
assert!(!path.join(format!("{id}.asset")).exists());
|
||||||
|
|
||||||
|
cache.compute_asset_cache_filename = Some(
|
||||||
|
AssetCacheComputeAssetCacheFilenameDelegate::from_handler(|directory, _| {
|
||||||
|
Ok(PathBuf::from(directory)
|
||||||
|
.join("..")
|
||||||
|
.join("escape")
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cache.has_asset(UUID::random().unwrap()),
|
||||||
|
Err(Error::Argument)
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cache_pruning_reclaims_old_entries_to_below_the_limit() {
|
||||||
|
let (_client, cache, path) = fixture(12);
|
||||||
|
cache
|
||||||
|
.save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![1; 8])
|
||||||
|
.unwrap();
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||||
|
cache
|
||||||
|
.save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![2; 8])
|
||||||
|
.unwrap();
|
||||||
|
cache.prune(None).await.unwrap();
|
||||||
|
let bytes: u64 = fs::read_dir(&path)
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| entry.metadata().ok())
|
||||||
|
.map(|metadata| metadata.len())
|
||||||
|
.sum();
|
||||||
|
assert!(bytes <= 10);
|
||||||
|
fs::remove_dir_all(path).unwrap();
|
||||||
|
}
|
||||||
@@ -120,6 +120,7 @@ struct ClientRuntime {
|
|||||||
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
|
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
|
||||||
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
|
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
|
||||||
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
|
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
|
||||||
|
asset_manager: Mutex<Option<Arc<crate::asset_manager::AssetManagerInner>>>,
|
||||||
shutdown_complete: Condvar,
|
shutdown_complete: Condvar,
|
||||||
shutdown_wait: Mutex<()>,
|
shutdown_wait: Mutex<()>,
|
||||||
}
|
}
|
||||||
@@ -150,6 +151,7 @@ impl ClientRuntime {
|
|||||||
agent_manager: Mutex::new(std::sync::Weak::new()),
|
agent_manager: Mutex::new(std::sync::Weak::new()),
|
||||||
inventory_manager: Mutex::new(None),
|
inventory_manager: Mutex::new(None),
|
||||||
inventory_ais_client: Mutex::new(None),
|
inventory_ais_client: Mutex::new(None),
|
||||||
|
asset_manager: Mutex::new(None),
|
||||||
shutdown_complete: Condvar::new(),
|
shutdown_complete: Condvar::new(),
|
||||||
shutdown_wait: Mutex::new(()),
|
shutdown_wait: Mutex::new(()),
|
||||||
}
|
}
|
||||||
@@ -447,6 +449,29 @@ impl GridClient {
|
|||||||
Ok(manager)
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn native_assets(&self) -> Result<crate::AssetManager, crate::Error> {
|
||||||
|
let mut cached = self
|
||||||
|
.runtime
|
||||||
|
.asset_manager
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
if let Some(inner) = cached.as_ref() {
|
||||||
|
return crate::asset_manager::AssetManager::native_from_inner(Arc::clone(inner));
|
||||||
|
}
|
||||||
|
let manager = crate::asset_manager::AssetManager::new(Some(self.clone()))?;
|
||||||
|
*cached = Some(manager.native_inner());
|
||||||
|
Ok(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub(crate) fn native_set_assets(&mut self, value: crate::AssetManager) {
|
||||||
|
*self
|
||||||
|
.runtime
|
||||||
|
.asset_manager
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn native_ais_client(&self) -> Result<crate::InventoryAISClient, crate::Error> {
|
pub(crate) fn native_ais_client(&self) -> Result<crate::InventoryAISClient, crate::Error> {
|
||||||
let mut cached = self
|
let mut cached = self
|
||||||
.runtime
|
.runtime
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
|
|
||||||
use crate::{Error, GridClient, HttpCapsClientProgressReport};
|
use crate::{Error, GridClient, HttpCapsClientProgressReport};
|
||||||
use libremetaverse_types::compat::{
|
use libremetaverse_types::compat::{
|
||||||
CancellationToken, CancellationTokenSource, HttpResponse, IProgress, Subscription,
|
CancellationToken, CancellationTokenSource, HttpResponse, IProgress, TaskCompletionSource, Uri,
|
||||||
TaskCompletionSource, Uri,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -82,7 +81,6 @@ impl fmt::Debug for DownloadRequest {
|
|||||||
|
|
||||||
struct ActiveDownload {
|
struct ActiveDownload {
|
||||||
cancellation: CancellationTokenSource,
|
cancellation: CancellationTokenSource,
|
||||||
cancellation_guards: Mutex<Vec<Subscription>>,
|
|
||||||
progress: Mutex<Vec<Arc<dyn IProgress<HttpCapsClientProgressReport>>>>,
|
progress: Mutex<Vec<Arc<dyn IProgress<HttpCapsClientProgressReport>>>>,
|
||||||
completion_sources: Mutex<Vec<DownloadCompletion>>,
|
completion_sources: Mutex<Vec<DownloadCompletion>>,
|
||||||
waiters: Mutex<Vec<oneshot::Sender<DownloadResult>>>,
|
waiters: Mutex<Vec<oneshot::Sender<DownloadResult>>>,
|
||||||
@@ -93,7 +91,6 @@ impl ActiveDownload {
|
|||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cancellation: CancellationTokenSource::new(),
|
cancellation: CancellationTokenSource::new(),
|
||||||
cancellation_guards: Mutex::new(Vec::new()),
|
|
||||||
progress: Mutex::new(Vec::new()),
|
progress: Mutex::new(Vec::new()),
|
||||||
completion_sources: Mutex::new(Vec::new()),
|
completion_sources: Mutex::new(Vec::new()),
|
||||||
waiters: Mutex::new(Vec::new()),
|
waiters: Mutex::new(Vec::new()),
|
||||||
@@ -101,12 +98,6 @@ impl ActiveDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attach_cancellation(&self, token: &CancellationToken) {
|
|
||||||
let cancellation = self.cancellation.clone();
|
|
||||||
let guard = token.register_callback(Arc::new(move || cancellation.cancel()));
|
|
||||||
mutex(&self.cancellation_guards).push(guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attach_progress(&self, progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>) {
|
fn attach_progress(&self, progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>) {
|
||||||
if let Some(progress) = progress {
|
if let Some(progress) = progress {
|
||||||
mutex(&self.progress).push(Arc::from(progress));
|
mutex(&self.progress).push(Arc::from(progress));
|
||||||
@@ -146,7 +137,6 @@ impl ActiveDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutex(&self.progress).clear();
|
mutex(&self.progress).clear();
|
||||||
mutex(&self.cancellation_guards).clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report(&self, report: HttpCapsClientProgressReport) {
|
fn report(&self, report: HttpCapsClientProgressReport) {
|
||||||
@@ -371,13 +361,8 @@ impl DownloadManager {
|
|||||||
request: DownloadRequest,
|
request: DownloadRequest,
|
||||||
) -> DownloadResult {
|
) -> DownloadResult {
|
||||||
let cancellation = request.cancellation_token.clone();
|
let cancellation = request.cancellation_token.clone();
|
||||||
let (active, receiver) = self.enqueue(request, true)?;
|
let (_, receiver) = self.enqueue(request, true)?;
|
||||||
await_download(
|
await_download(receiver.ok_or(Error::InvalidOperation)?, cancellation).await
|
||||||
active,
|
|
||||||
receiver.ok_or(Error::InvalidOperation)?,
|
|
||||||
cancellation,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn queue_download_with_uri_string_i_progress_cancellation_token_int32(
|
pub async fn queue_download_with_uri_string_i_progress_cancellation_token_int32(
|
||||||
@@ -392,13 +377,8 @@ impl DownloadManager {
|
|||||||
let mut request = DownloadRequest::new(address, content_type, progress_callback)?;
|
let mut request = DownloadRequest::new(address, content_type, progress_callback)?;
|
||||||
request.cancellation_token = cancellation.clone();
|
request.cancellation_token = cancellation.clone();
|
||||||
request.retries = retries.unwrap_or(5).max(0);
|
request.retries = retries.unwrap_or(5).max(0);
|
||||||
let (active, receiver) = self.enqueue(request, true)?;
|
let (_, receiver) = self.enqueue(request, true)?;
|
||||||
await_download(
|
await_download(receiver.ok_or(Error::InvalidOperation)?, cancellation).await
|
||||||
active,
|
|
||||||
receiver.ok_or(Error::InvalidOperation)?,
|
|
||||||
cancellation,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_with_uri_string_i_progress_cancellation_token(
|
pub async fn download_with_uri_string_i_progress_cancellation_token(
|
||||||
@@ -466,7 +446,6 @@ impl DownloadManager {
|
|||||||
}
|
}
|
||||||
return Err(Error::Cancelled);
|
return Err(Error::Cancelled);
|
||||||
};
|
};
|
||||||
active.attach_cancellation(&request.cancellation_token);
|
|
||||||
active.attach_progress(request.download_progress_callback.take());
|
active.attach_progress(request.download_progress_callback.take());
|
||||||
active.attach_completion_source(request.completion_tcs.take());
|
active.attach_completion_source(request.completion_tcs.take());
|
||||||
let receiver = with_waiter.then(|| active.add_waiter());
|
let receiver = with_waiter.then(|| active.add_waiter());
|
||||||
@@ -507,14 +486,12 @@ impl fmt::Debug for DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn await_download(
|
async fn await_download(
|
||||||
active: Arc<ActiveDownload>,
|
|
||||||
receiver: oneshot::Receiver<DownloadResult>,
|
receiver: oneshot::Receiver<DownloadResult>,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> DownloadResult {
|
) -> DownloadResult {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = receiver => result.map_err(|_| Error::Cancelled)?,
|
result = receiver => result.map_err(|_| Error::Cancelled)?,
|
||||||
() = cancellation.cancelled() => {
|
() = cancellation.cancelled() => {
|
||||||
active.cancellation.cancel();
|
|
||||||
Err(Error::Cancelled)
|
Err(Error::Cancelled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4396,6 +4396,117 @@ fn parse_task_inventory_values(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InventoryManager {
|
||||||
|
pub(crate) async fn native_update_material_inventory(
|
||||||
|
&self,
|
||||||
|
material: crate::assets::AssetMaterial,
|
||||||
|
material_item_id: UUID,
|
||||||
|
task_id: Option<UUID>,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
progress: Option<
|
||||||
|
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||||
|
>,
|
||||||
|
) -> Result<(bool, String, UUID, UUID), Error> {
|
||||||
|
let client = self.client()?;
|
||||||
|
let simulator = client
|
||||||
|
.native_network()?
|
||||||
|
.current_sim()
|
||||||
|
.ok_or(Error::InvalidOperation)?;
|
||||||
|
let caps = simulator.native_caps().ok_or(Error::InvalidOperation)?;
|
||||||
|
let name = if task_id.is_some() {
|
||||||
|
"UpdateMaterialTaskInventory"
|
||||||
|
} else {
|
||||||
|
"UpdateMaterialAgentInventory"
|
||||||
|
};
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
|
let uri = loop {
|
||||||
|
if let Some(uri) = caps.capability_uri(name.to_owned())? {
|
||||||
|
break uri;
|
||||||
|
}
|
||||||
|
if caps.seed_request_finished() || Instant::now() >= deadline {
|
||||||
|
return Err(Error::InvalidOperation);
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
};
|
||||||
|
let mut query = HashMap::from([("item_id".to_owned(), OSD::UUID(material_item_id))]);
|
||||||
|
if let Some(task_id) = task_id {
|
||||||
|
query.insert("task_id".to_owned(), OSD::UUID(task_id));
|
||||||
|
}
|
||||||
|
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(query))?;
|
||||||
|
let token = cancellation_token.unwrap_or_default();
|
||||||
|
let (response, metadata) = 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(&response.status_code) {
|
||||||
|
return Ok((
|
||||||
|
false,
|
||||||
|
format!("HTTP {}", response.status_code),
|
||||||
|
UUID::zero(),
|
||||||
|
UUID::zero(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let meta: serde_json::Value =
|
||||||
|
serde_json::from_slice(&metadata).map_err(|_| Error::Argument)?;
|
||||||
|
let status = meta
|
||||||
|
.get("state")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned();
|
||||||
|
let uploader = meta
|
||||||
|
.get("uploader")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.ok_or(Error::Argument)?
|
||||||
|
.to_owned();
|
||||||
|
let data = material.to_json()?.into_bytes();
|
||||||
|
let (upload_response, response_body) = client
|
||||||
|
.native_http_caps_client()
|
||||||
|
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||||
|
Uri(uploader),
|
||||||
|
"application/octet-stream".into(),
|
||||||
|
data,
|
||||||
|
token,
|
||||||
|
progress,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if !(200..300).contains(&upload_response.status_code) {
|
||||||
|
return Ok((
|
||||||
|
false,
|
||||||
|
format!("HTTP {}", upload_response.status_code),
|
||||||
|
material_item_id,
|
||||||
|
UUID::zero(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let result: serde_json::Value =
|
||||||
|
serde_json::from_slice(&response_body).map_err(|_| Error::Argument)?;
|
||||||
|
let final_status = result
|
||||||
|
.get("state")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.unwrap_or(&status)
|
||||||
|
.to_owned();
|
||||||
|
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()?
|
||||||
|
.unwrap_or_else(UUID::zero);
|
||||||
|
let item_id = result
|
||||||
|
.get("item_id")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.map(|id| UUID::new_with_string(id.to_owned()))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(material_item_id);
|
||||||
|
Ok((final_status == "complete", final_status, asset_id, item_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ mod attention_catalog;
|
|||||||
mod agent_manager;
|
mod agent_manager;
|
||||||
mod agent_messages;
|
mod agent_messages;
|
||||||
mod agent_movement;
|
mod agent_movement;
|
||||||
|
mod asset_cache;
|
||||||
|
mod asset_manager;
|
||||||
|
mod asset_material;
|
||||||
|
mod asset_models;
|
||||||
mod bit_pack;
|
mod bit_pack;
|
||||||
mod caps;
|
mod caps;
|
||||||
mod caps_http;
|
mod caps_http;
|
||||||
@@ -27,6 +31,7 @@ mod login;
|
|||||||
mod message_codec;
|
mod message_codec;
|
||||||
mod message_decoder;
|
mod message_decoder;
|
||||||
mod network_manager;
|
mod network_manager;
|
||||||
|
mod object_material;
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub mod packet_catalog;
|
pub mod packet_catalog;
|
||||||
mod packet_wire;
|
mod packet_wire;
|
||||||
@@ -35,6 +40,7 @@ mod skeleton;
|
|||||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||||
mod skeleton_catalog;
|
mod skeleton_catalog;
|
||||||
mod targa;
|
mod targa;
|
||||||
|
mod transfers;
|
||||||
mod udp_transport;
|
mod udp_transport;
|
||||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||||
mod visual_catalog;
|
mod visual_catalog;
|
||||||
@@ -44,6 +50,8 @@ mod appearance_baker_semantics;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod asset_archive_semantics;
|
mod asset_archive_semantics;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod asset_pipeline_semantics;
|
||||||
|
#[cfg(test)]
|
||||||
mod avatar_animesh_semantics;
|
mod avatar_animesh_semantics;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod batch_link_internal_semantics;
|
mod batch_link_internal_semantics;
|
||||||
|
|||||||
192
crates/libremetaverse/src/object_material.rs
Normal file
192
crates/libremetaverse/src/object_material.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
//! `ModifyMaterialParams` capability integration.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
clippy::missing_errors_doc,
|
||||||
|
clippy::must_use_candidate,
|
||||||
|
clippy::option_option
|
||||||
|
)]
|
||||||
|
|
||||||
|
use crate::assets::AssetMaterial;
|
||||||
|
use crate::{Error, GridClient, Simulator};
|
||||||
|
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||||
|
use libremetaverse_types::{UUID, compat::CancellationToken};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GLTFMaterialUpdate {
|
||||||
|
asset_id: Option<Option<UUID>>,
|
||||||
|
object_id: UUID,
|
||||||
|
override_: Option<AssetMaterial>,
|
||||||
|
side: i32,
|
||||||
|
}
|
||||||
|
impl GLTFMaterialUpdate {
|
||||||
|
pub fn new() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
asset_id: None,
|
||||||
|
object_id: UUID::zero(),
|
||||||
|
override_: None,
|
||||||
|
side: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn asset_id(&self) -> Option<Option<UUID>> {
|
||||||
|
self.asset_id
|
||||||
|
}
|
||||||
|
pub fn set_asset_id(&mut self, value: Option<Option<UUID>>) {
|
||||||
|
self.asset_id = value;
|
||||||
|
}
|
||||||
|
pub const fn object_id(&self) -> UUID {
|
||||||
|
self.object_id
|
||||||
|
}
|
||||||
|
pub fn set_object_id(&mut self, value: UUID) {
|
||||||
|
self.object_id = value;
|
||||||
|
}
|
||||||
|
pub fn override_(&self) -> Option<AssetMaterial> {
|
||||||
|
self.override_.clone()
|
||||||
|
}
|
||||||
|
pub fn set_override_(&mut self, value: Option<AssetMaterial>) {
|
||||||
|
self.override_ = value;
|
||||||
|
}
|
||||||
|
pub const fn side(&self) -> i32 {
|
||||||
|
self.side
|
||||||
|
}
|
||||||
|
pub fn set_side(&mut self, value: i32) {
|
||||||
|
self.side = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ObjectManager {
|
||||||
|
client: crate::client_core::ClientWeakHandle,
|
||||||
|
}
|
||||||
|
impl ObjectManager {
|
||||||
|
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
client: client.ok_or(Error::ArgumentNull)?.native_weak_handle(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn client(&self) -> Result<GridClient, Error> {
|
||||||
|
self.client.upgrade().ok_or(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn native_send_material_updates(
|
||||||
|
&self,
|
||||||
|
sim: Simulator,
|
||||||
|
updates: Box<dyn Iterator<Item = GLTFMaterialUpdate>>,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let Some(caps) = sim.native_caps() else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
|
let uri = loop {
|
||||||
|
if let Some(uri) = caps.capability_uri("ModifyMaterialParams".into())? {
|
||||||
|
break uri;
|
||||||
|
}
|
||||||
|
if caps.seed_request_finished() || Instant::now() >= deadline {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
};
|
||||||
|
let mut body = Vec::new();
|
||||||
|
for update in updates {
|
||||||
|
if update.side < 0 {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let mut entry = HashMap::from([
|
||||||
|
("object_id".into(), OSD::UUID(update.object_id)),
|
||||||
|
("side".into(), OSD::Integer(update.side)),
|
||||||
|
]);
|
||||||
|
if let Some(Some(asset_id)) = update.asset_id {
|
||||||
|
entry.insert("asset_id".into(), OSD::UUID(asset_id));
|
||||||
|
}
|
||||||
|
if let Some(material) = update.override_ {
|
||||||
|
entry.insert("gltf_json".into(), OSD::String(material.to_json()?));
|
||||||
|
} else if update.asset_id.flatten().is_none() {
|
||||||
|
entry.insert("gltf_json".into(), OSD::String(String::new()));
|
||||||
|
}
|
||||||
|
body.push(OSD::Map(entry));
|
||||||
|
}
|
||||||
|
if body.is_empty() {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Array(body))?;
|
||||||
|
let client = self.client()?;
|
||||||
|
let (response, bytes) = client
|
||||||
|
.native_http_caps_client()
|
||||||
|
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||||
|
uri,
|
||||||
|
"application/llsd+xml".into(),
|
||||||
|
payload,
|
||||||
|
cancellation_token.unwrap_or_default(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if !(200..300).contains(&response.status_code) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let response: serde_json::Value =
|
||||||
|
serde_json::from_slice(&bytes).map_err(|_| Error::Argument)?;
|
||||||
|
Ok(response
|
||||||
|
.get("success")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(false))
|
||||||
|
}
|
||||||
|
pub(crate) async fn native_set_material_override(
|
||||||
|
&self,
|
||||||
|
sim: Simulator,
|
||||||
|
object_id: UUID,
|
||||||
|
side: i32,
|
||||||
|
override_material: AssetMaterial,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let mut update = GLTFMaterialUpdate::new()?;
|
||||||
|
update.object_id = object_id;
|
||||||
|
update.side = side;
|
||||||
|
update.override_ = Some(override_material);
|
||||||
|
self.native_send_material_updates(
|
||||||
|
sim,
|
||||||
|
Box::new(std::iter::once(update)),
|
||||||
|
cancellation_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub(crate) async fn native_clear_material_override(
|
||||||
|
&self,
|
||||||
|
sim: Simulator,
|
||||||
|
object_id: UUID,
|
||||||
|
side: i32,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let mut update = GLTFMaterialUpdate::new()?;
|
||||||
|
update.object_id = object_id;
|
||||||
|
update.side = side;
|
||||||
|
self.native_send_material_updates(
|
||||||
|
sim,
|
||||||
|
Box::new(std::iter::once(update)),
|
||||||
|
cancellation_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub(crate) async fn native_apply_material(
|
||||||
|
&self,
|
||||||
|
sim: Simulator,
|
||||||
|
object_id: UUID,
|
||||||
|
side: i32,
|
||||||
|
material_asset_id: UUID,
|
||||||
|
override_material: Option<AssetMaterial>,
|
||||||
|
cancellation_token: Option<CancellationToken>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let mut update = GLTFMaterialUpdate::new()?;
|
||||||
|
update.object_id = object_id;
|
||||||
|
update.side = side;
|
||||||
|
update.asset_id = Some(Some(material_asset_id));
|
||||||
|
update.override_ = override_material;
|
||||||
|
self.native_send_material_updates(
|
||||||
|
sim,
|
||||||
|
Box::new(std::iter::once(update)),
|
||||||
|
cancellation_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
226
crates/libremetaverse/src/transfers.rs
Normal file
226
crates/libremetaverse/src/transfers.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
//! Asset transfer state shared by HTTP and LLUDP pipelines.
|
||||||
|
|
||||||
|
#![allow(clippy::missing_errors_doc, clippy::must_use_candidate)]
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ChannelType, Error, ImageCodec, ImageType, Simulator, SourceType, StatusCode, TargetType,
|
||||||
|
TransferError,
|
||||||
|
};
|
||||||
|
use libremetaverse_types::compat::SortedList;
|
||||||
|
use libremetaverse_types::{AssetType, UUID};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Transfer {
|
||||||
|
pub asset_data: Vec<u8>,
|
||||||
|
pub asset_type: AssetType,
|
||||||
|
pub id: UUID,
|
||||||
|
pub size: i32,
|
||||||
|
pub success: bool,
|
||||||
|
pub transferred: i32,
|
||||||
|
pub(crate) last_packet: Instant,
|
||||||
|
}
|
||||||
|
impl Transfer {
|
||||||
|
pub fn new() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
asset_data: Vec::new(),
|
||||||
|
asset_type: AssetType::Unknown,
|
||||||
|
id: UUID::zero(),
|
||||||
|
size: 0,
|
||||||
|
success: false,
|
||||||
|
transferred: 0,
|
||||||
|
last_packet: Instant::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn time_since_last_packet(&self) -> i32 {
|
||||||
|
i32::try_from(self.last_packet.elapsed().as_millis()).unwrap_or(i32::MAX)
|
||||||
|
}
|
||||||
|
pub fn set_time_since_last_packet(&mut self, value: i32) {
|
||||||
|
self.last_packet = Instant::now()
|
||||||
|
.checked_sub(std::time::Duration::from_millis(
|
||||||
|
u64::try_from(value.max(0)).unwrap_or(0),
|
||||||
|
))
|
||||||
|
.unwrap_or_else(Instant::now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AssetDownload {
|
||||||
|
pub asset_id: UUID,
|
||||||
|
pub channel: ChannelType,
|
||||||
|
pub priority: f32,
|
||||||
|
pub simulator: Option<Simulator>,
|
||||||
|
pub source: SourceType,
|
||||||
|
pub status: StatusCode,
|
||||||
|
pub target: TargetType,
|
||||||
|
pub next_packet: i32,
|
||||||
|
pub out_of_order_packets: RwLock<HashMap<i32, Vec<u8>>>,
|
||||||
|
pub base: Transfer,
|
||||||
|
}
|
||||||
|
impl AssetDownload {
|
||||||
|
pub fn new() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
asset_id: UUID::zero(),
|
||||||
|
channel: ChannelType::Asset,
|
||||||
|
priority: 0.0,
|
||||||
|
simulator: None,
|
||||||
|
source: SourceType::Asset,
|
||||||
|
status: StatusCode::Unknown,
|
||||||
|
target: TargetType::Unknown,
|
||||||
|
next_packet: 0,
|
||||||
|
out_of_order_packets: RwLock::new(HashMap::new()),
|
||||||
|
base: Transfer::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ImageDownload {
|
||||||
|
pub codec: ImageCodec,
|
||||||
|
pub discard_level: i32,
|
||||||
|
pub image_type: ImageType,
|
||||||
|
pub packet_count: u16,
|
||||||
|
pub packets_seen: SortedList<u16, u16>,
|
||||||
|
pub priority: f32,
|
||||||
|
pub simulator: Option<Simulator>,
|
||||||
|
pub base: Transfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageDownload {
|
||||||
|
pub fn new() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
codec: ImageCodec::Invalid,
|
||||||
|
discard_level: 0,
|
||||||
|
image_type: ImageType::Normal,
|
||||||
|
packet_count: 0,
|
||||||
|
packets_seen: SortedList(std::collections::BTreeMap::new()),
|
||||||
|
priority: 0.0,
|
||||||
|
simulator: None,
|
||||||
|
base: Transfer::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetUpload {
|
||||||
|
pub asset_id: UUID,
|
||||||
|
pub packet_num: u32,
|
||||||
|
pub type_: AssetType,
|
||||||
|
pub xfer_id: u64,
|
||||||
|
pub base: Transfer,
|
||||||
|
}
|
||||||
|
impl AssetUpload {
|
||||||
|
pub fn new() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
asset_id: UUID::zero(),
|
||||||
|
packet_num: 0,
|
||||||
|
type_: AssetType::Unknown,
|
||||||
|
xfer_id: 0,
|
||||||
|
base: Transfer::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AssetUploadEventArgs {
|
||||||
|
upload: AssetUpload,
|
||||||
|
}
|
||||||
|
impl AssetUploadEventArgs {
|
||||||
|
pub fn new(upload: AssetUpload) -> Result<Self, Error> {
|
||||||
|
Ok(Self { upload })
|
||||||
|
}
|
||||||
|
pub fn upload(&self) -> AssetUpload {
|
||||||
|
self.upload.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ImageReceiveProgressEventArgs {
|
||||||
|
image_id: UUID,
|
||||||
|
received: i32,
|
||||||
|
total: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageReceiveProgressEventArgs {
|
||||||
|
pub fn new(image_id: UUID, received: i32, total: i32) -> Result<Self, Error> {
|
||||||
|
if received < 0 || total < 0 || received > total {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
image_id,
|
||||||
|
received,
|
||||||
|
total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub const fn image_id(&self) -> UUID {
|
||||||
|
self.image_id
|
||||||
|
}
|
||||||
|
pub const fn received(&self) -> i32 {
|
||||||
|
self.received
|
||||||
|
}
|
||||||
|
pub const fn total(&self) -> i32 {
|
||||||
|
self.total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct InitiateDownloadEventArgs {
|
||||||
|
sim_filename: String,
|
||||||
|
viewer_filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InitiateDownloadEventArgs {
|
||||||
|
pub fn new(sim_filename: String, viewer_filename: String) -> Result<Self, Error> {
|
||||||
|
if sim_filename.as_bytes().contains(&0) || viewer_filename.as_bytes().contains(&0) {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
sim_filename,
|
||||||
|
viewer_filename,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn sim_file_name(&self) -> String {
|
||||||
|
self.sim_filename.clone()
|
||||||
|
}
|
||||||
|
pub fn viewer_file_name(&self) -> String {
|
||||||
|
self.viewer_filename.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct XferDownload {
|
||||||
|
pub error: TransferError,
|
||||||
|
pub filename: String,
|
||||||
|
pub packet_num: u32,
|
||||||
|
pub v_file_id: UUID,
|
||||||
|
pub xfer_id: u64,
|
||||||
|
pub base: Transfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XferDownload {
|
||||||
|
pub fn new() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
error: TransferError::None,
|
||||||
|
filename: String::new(),
|
||||||
|
packet_num: 0,
|
||||||
|
v_file_id: UUID::zero(),
|
||||||
|
xfer_id: 0,
|
||||||
|
base: Transfer::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct XferReceivedEventArgs {
|
||||||
|
xfer: XferDownload,
|
||||||
|
}
|
||||||
|
impl XferReceivedEventArgs {
|
||||||
|
pub fn new(xfer: XferDownload) -> Result<Self, Error> {
|
||||||
|
Ok(Self { xfer })
|
||||||
|
}
|
||||||
|
pub fn xfer(&self) -> XferDownload {
|
||||||
|
self.xfer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -600,14 +600,17 @@ async fn download_manager_retries_transient_but_not_permanent_statuses() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn deduplicated_subscriber_cancellation_cancels_the_shared_download() {
|
async fn deduplicated_subscriber_cancellation_is_independent() {
|
||||||
let attempts = Arc::new(AtomicUsize::new(0));
|
let attempts = Arc::new(AtomicUsize::new(0));
|
||||||
let handler_attempts = Arc::clone(&attempts);
|
let handler_attempts = Arc::clone(&attempts);
|
||||||
let handler = HttpMessageHandler::new(move |_request, cancellation| {
|
let release = Arc::new(tokio::sync::Notify::new());
|
||||||
|
let handler_release = Arc::clone(&release);
|
||||||
|
let handler = HttpMessageHandler::new(move |_request, _cancellation| {
|
||||||
handler_attempts.fetch_add(1, Ordering::AcqRel);
|
handler_attempts.fetch_add(1, Ordering::AcqRel);
|
||||||
|
let release = Arc::clone(&handler_release);
|
||||||
async move {
|
async move {
|
||||||
cancellation.cancelled().await;
|
release.notified().await;
|
||||||
response(200, b"too late".to_vec())
|
response(200, b"shared".to_vec())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut client = GridClient::new().expect("client");
|
let mut client = GridClient::new().expect("client");
|
||||||
@@ -645,8 +648,12 @@ async fn deduplicated_subscriber_cancellation_cancels_the_shared_download() {
|
|||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
}
|
}
|
||||||
cancellation.cancel();
|
cancellation.cancel();
|
||||||
assert_eq!(first.await.expect("first task"), Err(Error::Cancelled));
|
|
||||||
assert_eq!(second.await.expect("second task"), Err(Error::Cancelled));
|
assert_eq!(second.await.expect("second task"), Err(Error::Cancelled));
|
||||||
|
release.notify_waiters();
|
||||||
|
assert_eq!(
|
||||||
|
first.await.expect("first task").expect("first download").1,
|
||||||
|
b"shared"
|
||||||
|
);
|
||||||
assert_eq!(attempts.load(Ordering::Acquire), 1);
|
assert_eq!(attempts.load(Ordering::Acquire), 1);
|
||||||
downloads.dispose().expect("dispose");
|
downloads.dispose().expect("dispose");
|
||||||
}
|
}
|
||||||
|
|||||||
49
docs/assets.md
Normal file
49
docs/assets.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Asset models, transfers, and cache
|
||||||
|
|
||||||
|
The native asset layer owns the client-facing asset boundary from typed bytes to
|
||||||
|
transport and persistence. `Asset` retains its UUID, asset type, temporary flag,
|
||||||
|
and mutable raw payload. Concrete animation, sound, script, calling-card,
|
||||||
|
landmark, settings, mesh, texture, wearable, and notecard models enforce a 64 MiB
|
||||||
|
input ceiling and reject malformed structured formats with typed errors. Mesh and
|
||||||
|
settings parsing use StructuredData. Texture encode/decode uses the imaging
|
||||||
|
crate's bounded JPEG 2000 adapter, while PCM conversion streams bounded planar
|
||||||
|
blocks through the cross-platform Ogg Vorbis encoder; neither API exposes codec
|
||||||
|
implementation types.
|
||||||
|
|
||||||
|
`AssetManager` is client-owned and shares one `DownloadManager` and `AssetCache`.
|
||||||
|
ViewerAsset, GetMesh, GetTexture, and server-bake requests check the cache before
|
||||||
|
network I/O. Equal HTTP URIs share one in-flight request and its progress sinks;
|
||||||
|
each awaiting subscriber retains an independent cancellation token, so cancelling
|
||||||
|
one waiter cannot cancel the shared request for the others. Capability discovery
|
||||||
|
waits for the seed request's bounded completion rather than racing it.
|
||||||
|
|
||||||
|
When ViewerAsset is unavailable, asset and authenticated inventory requests use
|
||||||
|
LLUDP `TransferRequest`, `TransferInfo`, and `TransferPacket`. The receiver filters
|
||||||
|
by transfer UUID, preserves priority and source type, reassembles out-of-order
|
||||||
|
packets, limits announced data to 64 MiB, and sends `TransferAbort` on cancellation
|
||||||
|
or timeout. Inventory transfers include the agent, session, owner, task, item,
|
||||||
|
asset, and type parameters. Legacy Xfer downloads confirm each packet, enforce the
|
||||||
|
same bound, and publish only completely assembled results.
|
||||||
|
|
||||||
|
Small and large uploads use the simulator's `AssetUploadRequest`/Xfer handshake,
|
||||||
|
including packet confirmation and progress events. Baked texture and inventory or
|
||||||
|
task material uploads use their two-stage capability contracts and require a
|
||||||
|
complete response containing the new asset UUID. Upload size, cancellation, and
|
||||||
|
timeout failures remove their pending correlation state.
|
||||||
|
|
||||||
|
The disk cache uses UUID-only filenames by default and confines custom filename
|
||||||
|
callbacks to the configured cache directory. Reads reject empty, oversized, or
|
||||||
|
non-regular files. Writes use uniquely named same-directory files, flush them,
|
||||||
|
and atomically rename them into place, making concurrent writers deterministic.
|
||||||
|
Pruning removes least-recently-accessed regular cache entries until usage is below 90% of
|
||||||
|
the configured maximum. Automatic pruning is activity-driven at the configured
|
||||||
|
millisecond interval, avoiding a permanent timer thread; cancellation is checked
|
||||||
|
between deletions. Clearing and pruning never inspect or delete files outside the
|
||||||
|
configured project/application cache directory.
|
||||||
|
|
||||||
|
Focused native tests in `asset_pipeline_semantics` cover real sound and texture
|
||||||
|
codec output, model typing, malformed mesh data, atomic cache replacement, cache
|
||||||
|
corruption, cached-image reconstruction, and pruning. The `caps_http`
|
||||||
|
deduplication case proves independent subscriber cancellation. Translated asset,
|
||||||
|
material, mesh, capability-upload, and cache compatibility cases provide the API
|
||||||
|
contract checks for this issue.
|
||||||
60
tests/api-compile/Cargo.lock
generated
60
tests/api-compile/Cargo.lock
generated
@@ -17,6 +17,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aotuv_lancer_vorbis_sys"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bc4fd1a61860d2f1198b60bedd30910eaffa978f1ee6214dfb24ac70d589225"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"ogg_next_sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -285,6 +295,16 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
@@ -725,13 +745,16 @@ dependencies = [
|
|||||||
"os_info",
|
"os_info",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"vorbis_rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libremetaverse-imaging"
|
name = "libremetaverse-imaging"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"libremetaverse-openjpeg",
|
||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -750,6 +773,14 @@ dependencies = [
|
|||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libremetaverse-openjpeg"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libremetaverse-prim-mesher"
|
name = "libremetaverse-prim-mesher"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -1113,6 +1144,15 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ogg_next_sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2d7a48e247c2bb07e633aefb65a38648ea58c7eedd4e4408a5861721ab049b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -1877,12 +1917,32 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vorbis_rs"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49c5da94d280f7a27e8c937e9b73df2da3e23a2583f48471fd8fb4c72f9c1933"
|
||||||
|
dependencies = [
|
||||||
|
"aotuv_lancer_vorbis_sys",
|
||||||
|
"errno",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"ogg_next_sys",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
62
tools/check_milestone_09_issue_64.py
Normal file
62
tools/check_milestone_09_issue_64.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit issue 64's native asset, transfer, cache, and download boundary."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
import generate_api_shims
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SOURCES = tuple(
|
||||||
|
ROOT / "crates" / "libremetaverse" / "src" / name
|
||||||
|
for name in (
|
||||||
|
"asset_cache.rs", "asset_manager.rs", "asset_material.rs",
|
||||||
|
"asset_models.rs", "object_material.rs", "transfers.rs",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
DOC = ROOT / "docs" / "assets.md"
|
||||||
|
NATIVE_TESTS = ROOT / "crates" / "libremetaverse" / "src" / "asset_pipeline_semantics.rs"
|
||||||
|
HTTP_TESTS = ROOT / "crates" / "libremetaverse" / "tests" / "caps_http.rs"
|
||||||
|
STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(")
|
||||||
|
|
||||||
|
REQUIRED_TYPES = {
|
||||||
|
"T:LibreMetaverse.AssetCache", "T:LibreMetaverse.AssetManager",
|
||||||
|
"T:LibreMetaverse.AssetDownload", "T:LibreMetaverse.AssetUpload",
|
||||||
|
"T:LibreMetaverse.AssetUploadEventArgs", "T:LibreMetaverse.ImageDownload",
|
||||||
|
"T:LibreMetaverse.ImageReceiveProgressEventArgs",
|
||||||
|
"T:LibreMetaverse.InitiateDownloadEventArgs", "T:LibreMetaverse.Transfer",
|
||||||
|
"T:LibreMetaverse.XferDownload", "T:LibreMetaverse.XferReceivedEventArgs",
|
||||||
|
"T:LibreMetaverse.Assets.Asset", "T:LibreMetaverse.Assets.AssetMaterial",
|
||||||
|
"T:LibreMetaverse.Assets.AssetMesh", "T:LibreMetaverse.Assets.AssetTexture",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def require_markers(path: Path, markers: tuple[str, ...]) -> None:
|
||||||
|
text = path.read_text()
|
||||||
|
missing = [marker for marker in markers if marker not in text]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
native = set(generate_api_shims.NATIVE_TYPES) | set(generate_api_shims.NATIVE_DECLARATIONS)
|
||||||
|
missing = sorted(REQUIRED_TYPES - native)
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("issue 64 native types missing: " + ", ".join(missing))
|
||||||
|
for source in SOURCES:
|
||||||
|
if STUB_RE.search(source.read_text()):
|
||||||
|
raise SystemExit(f"issue 64 owned Rust stubs remain in {source.name}")
|
||||||
|
require_markers(SOURCES[0], ("create_new(true)", "fs::rename", "CACHE_TARGET_PERCENT", "maybe_begin_prune"))
|
||||||
|
require_markers(SOURCES[1], ("ViewerAsset", "TransferRequest", "TransferAbort", "UploadBakedTexture", "AssetUploadRequest"))
|
||||||
|
require_markers(SOURCES[3], ("MAX_ASSET_BYTES", "VorbisEncoderBuilder", "J2kCodec", "serialize_llsd_binary"))
|
||||||
|
require_markers(NATIVE_TESTS, ("sound_and_texture_codecs_produce_real_valid_payloads", "cache_writes_are_atomic_and_corruption_is_typed", "cache_pruning_reclaims_old_entries"))
|
||||||
|
require_markers(HTTP_TESTS, ("deduplicated_subscriber_cancellation_is_independent", "attempts.load(Ordering::Acquire), 1"))
|
||||||
|
require_markers(DOC, ("64 MiB", "independent cancellation", "out-of-order", "atomically rename", "90%"))
|
||||||
|
print("issue 64 audit: native models/codecs, bounded LLUDP and capability transfers, independent dedup cancellation, atomic cache, documentation, and focused tests are present")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -105,6 +105,36 @@ NATIVE_TYPES = {
|
|||||||
"T:LibreMetaverse.ChatSessionMemberLeftEventArgs": "crate::agent_messages::ChatSessionMemberLeftEventArgs",
|
"T:LibreMetaverse.ChatSessionMemberLeftEventArgs": "crate::agent_messages::ChatSessionMemberLeftEventArgs",
|
||||||
"T:LibreMetaverse.CameraConstraintEventArgs": "crate::agent_movement::CameraConstraintEventArgs",
|
"T:LibreMetaverse.CameraConstraintEventArgs": "crate::agent_movement::CameraConstraintEventArgs",
|
||||||
"T:LibreMetaverse.Assets.AssetGesture": "crate::gesture::AssetGesture",
|
"T:LibreMetaverse.Assets.AssetGesture": "crate::gesture::AssetGesture",
|
||||||
|
"T:LibreMetaverse.AssetCache": "crate::asset_cache::AssetCache",
|
||||||
|
"T:LibreMetaverse.AssetCache.ComputeAssetCacheFilenameDelegate": "crate::asset_cache::AssetCacheComputeAssetCacheFilenameDelegate",
|
||||||
|
"T:LibreMetaverse.AssetManager": "crate::asset_manager::AssetManager",
|
||||||
|
"T:LibreMetaverse.AssetDownload": "crate::transfers::AssetDownload",
|
||||||
|
"T:LibreMetaverse.ImageDownload": "crate::transfers::ImageDownload",
|
||||||
|
"T:LibreMetaverse.AssetUpload": "crate::transfers::AssetUpload",
|
||||||
|
"T:LibreMetaverse.AssetUploadEventArgs": "crate::transfers::AssetUploadEventArgs",
|
||||||
|
"T:LibreMetaverse.Transfer": "crate::transfers::Transfer",
|
||||||
|
"T:LibreMetaverse.ImageReceiveProgressEventArgs": "crate::transfers::ImageReceiveProgressEventArgs",
|
||||||
|
"T:LibreMetaverse.InitiateDownloadEventArgs": "crate::transfers::InitiateDownloadEventArgs",
|
||||||
|
"T:LibreMetaverse.XferDownload": "crate::transfers::XferDownload",
|
||||||
|
"T:LibreMetaverse.XferReceivedEventArgs": "crate::transfers::XferReceivedEventArgs",
|
||||||
|
"T:LibreMetaverse.Assets.Asset": "crate::asset_models::Asset",
|
||||||
|
"T:LibreMetaverse.Assets.AssetAnimation": "crate::asset_models::AssetAnimation",
|
||||||
|
"T:LibreMetaverse.Assets.AssetBodypart": "crate::asset_models::AssetBodypart",
|
||||||
|
"T:LibreMetaverse.Assets.AssetCallingCard": "crate::asset_models::AssetCallingCard",
|
||||||
|
"T:LibreMetaverse.Assets.AssetClothing": "crate::asset_models::AssetClothing",
|
||||||
|
"T:LibreMetaverse.Assets.AssetLandmark": "crate::asset_models::AssetLandmark",
|
||||||
|
"T:LibreMetaverse.Assets.AssetMaterial": "crate::asset_material::AssetMaterial",
|
||||||
|
"T:LibreMetaverse.Assets.AssetMesh": "crate::asset_models::AssetMesh",
|
||||||
|
"T:LibreMetaverse.Assets.AssetMutable": "crate::asset_models::AssetMutable",
|
||||||
|
"T:LibreMetaverse.Assets.AssetNotecard": "crate::asset_models::AssetNotecard",
|
||||||
|
"T:LibreMetaverse.Assets.AssetScriptBinary": "crate::asset_models::AssetScriptBinary",
|
||||||
|
"T:LibreMetaverse.Assets.AssetScriptText": "crate::asset_models::AssetScriptText",
|
||||||
|
"T:LibreMetaverse.Assets.AssetSettings": "crate::asset_models::AssetSettings",
|
||||||
|
"T:LibreMetaverse.Assets.AssetSound": "crate::asset_models::AssetSound",
|
||||||
|
"T:LibreMetaverse.Assets.AssetTexture": "crate::asset_models::AssetTexture",
|
||||||
|
"T:LibreMetaverse.Assets.AssetWearable": "crate::asset_models::AssetWearable",
|
||||||
|
"T:LibreMetaverse.Assets.GltfTextureTransform": "crate::asset_material::GltfTextureTransform",
|
||||||
|
"T:LibreMetaverse.GLTFMaterialUpdate": "crate::object_material::GLTFMaterialUpdate",
|
||||||
"T:LibreMetaverse.Assets.GestureStep": "crate::gesture::GestureStep",
|
"T:LibreMetaverse.Assets.GestureStep": "crate::gesture::GestureStep",
|
||||||
"T:LibreMetaverse.Assets.GestureStepAnimation": "crate::gesture::GestureStepAnimation",
|
"T:LibreMetaverse.Assets.GestureStepAnimation": "crate::gesture::GestureStepAnimation",
|
||||||
"T:LibreMetaverse.Assets.GestureStepChat": "crate::gesture::GestureStepChat",
|
"T:LibreMetaverse.Assets.GestureStepChat": "crate::gesture::GestureStepChat",
|
||||||
@@ -265,6 +295,7 @@ NATIVE_DECLARATIONS = {
|
|||||||
"T:LibreMetaverse.InventoryManager": "crate::inventory_manager::InventoryManager",
|
"T:LibreMetaverse.InventoryManager": "crate::inventory_manager::InventoryManager",
|
||||||
"T:LibreMetaverse.InventoryAISClient": "crate::inventory_ais::InventoryAISClient",
|
"T:LibreMetaverse.InventoryAISClient": "crate::inventory_ais::InventoryAISClient",
|
||||||
"T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager",
|
"T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager",
|
||||||
|
"T:LibreMetaverse.ObjectManager": "crate::object_material::ObjectManager",
|
||||||
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
|
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
|
||||||
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
|
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
|
||||||
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
|
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
|
||||||
@@ -335,6 +366,14 @@ NATIVE_MEMBER_BODIES = {
|
|||||||
"self.native_http_caps_client()",
|
"self.native_http_caps_client()",
|
||||||
"P:LibreMetaverse.GridClient.HttpCapsClient#set":
|
"P:LibreMetaverse.GridClient.HttpCapsClient#set":
|
||||||
"self.native_set_http_caps_client(value)",
|
"self.native_set_http_caps_client(value)",
|
||||||
|
"P:LibreMetaverse.GridClient.Assets":
|
||||||
|
"self.native_assets().unwrap_or_else(|_| panic!(\"failed to construct AssetManager\"))",
|
||||||
|
"P:LibreMetaverse.GridClient.Assets#set":
|
||||||
|
"self.native_set_assets(value)",
|
||||||
|
"P:LibreMetaverse.GridClient.Objects":
|
||||||
|
"crate::object_material::ObjectManager::native_new(Some(std::sync::Arc::new(self.clone()))).unwrap_or_else(|_| panic!(\"failed to construct ObjectManager\"))",
|
||||||
|
"P:LibreMetaverse.GridClient.Objects#set":
|
||||||
|
"{ let _ = value; }",
|
||||||
"P:LibreMetaverse.GridClient.Inventory":
|
"P:LibreMetaverse.GridClient.Inventory":
|
||||||
"self.native_inventory().unwrap_or_else(|_| panic!(\"failed to construct InventoryManager\"))",
|
"self.native_inventory().unwrap_or_else(|_| panic!(\"failed to construct InventoryManager\"))",
|
||||||
"P:LibreMetaverse.GridClient.Inventory#set":
|
"P:LibreMetaverse.GridClient.Inventory#set":
|
||||||
@@ -375,6 +414,20 @@ NATIVE_MEMBER_BODIES = {
|
|||||||
"crate::inventory_ais::InventoryAISClient::LIBRARY_CAP_NAME",
|
"crate::inventory_ais::InventoryAISClient::LIBRARY_CAP_NAME",
|
||||||
"M:LibreMetaverse.InventoryAISClient.#ctor(LibreMetaverse.GridClient)":
|
"M:LibreMetaverse.InventoryAISClient.#ctor(LibreMetaverse.GridClient)":
|
||||||
"crate::inventory_ais::InventoryAISClient::native_new(client)",
|
"crate::inventory_ais::InventoryAISClient::native_new(client)",
|
||||||
|
"M:LibreMetaverse.ObjectManager.#ctor(LibreMetaverse.GridClient)":
|
||||||
|
"crate::object_material::ObjectManager::native_new(client)",
|
||||||
|
"M:LibreMetaverse.ObjectManager.SendMaterialUpdatesAsync(LibreMetaverse.Simulator,System.Collections.Generic.IEnumerable{LibreMetaverse.GLTFMaterialUpdate},System.Threading.CancellationToken)":
|
||||||
|
"self.native_send_material_updates(sim, updates, cancellation_token).await",
|
||||||
|
"M:LibreMetaverse.ObjectManager.SetMaterialOverrideAsync(LibreMetaverse.Simulator,LibreMetaverse.UUID,System.Int32,LibreMetaverse.Assets.AssetMaterial,System.Threading.CancellationToken)":
|
||||||
|
"self.native_set_material_override(sim, object_id, side, override_material, cancellation_token).await",
|
||||||
|
"M:LibreMetaverse.ObjectManager.ClearMaterialOverrideAsync(LibreMetaverse.Simulator,LibreMetaverse.UUID,System.Int32,System.Threading.CancellationToken)":
|
||||||
|
"self.native_clear_material_override(sim, object_id, side, cancellation_token).await",
|
||||||
|
"M:LibreMetaverse.ObjectManager.ApplyMaterialAsync(LibreMetaverse.Simulator,LibreMetaverse.UUID,System.Int32,LibreMetaverse.UUID,LibreMetaverse.Assets.AssetMaterial,System.Threading.CancellationToken)":
|
||||||
|
"self.native_apply_material(sim, object_id, side, material_asset_id, override_material, cancellation_token).await",
|
||||||
|
"M:LibreMetaverse.InventoryManager.RequestUpdateMaterialAgentInventoryAsync(LibreMetaverse.Assets.AssetMaterial,LibreMetaverse.UUID,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})":
|
||||||
|
"self.native_update_material_inventory(material, material_item_id, None, cancellation_token, progress).await",
|
||||||
|
"M:LibreMetaverse.InventoryManager.RequestUpdateMaterialTaskInventoryAsync(LibreMetaverse.Assets.AssetMaterial,LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})":
|
||||||
|
"self.native_update_material_inventory(material, material_item_id, Some(task_id), cancellation_token, progress).await",
|
||||||
"P:LibreMetaverse.InventoryAISClient.IsAvailable": "self.native_is_available()",
|
"P:LibreMetaverse.InventoryAISClient.IsAvailable": "self.native_is_available()",
|
||||||
"M:LibreMetaverse.InventoryAISClient.CreateInventoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Boolean,System.Threading.CancellationToken)":
|
"M:LibreMetaverse.InventoryAISClient.CreateInventoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Boolean,System.Threading.CancellationToken)":
|
||||||
"self.native_create_inventory(parent_uuid, new_inventory, create_link, cancellation_token).await",
|
"self.native_create_inventory(parent_uuid, new_inventory, create_link, cancellation_token).await",
|
||||||
|
|||||||
@@ -985,6 +985,10 @@ def validate_generated_shims() -> None:
|
|||||||
"crate::agent_manager::",
|
"crate::agent_manager::",
|
||||||
"crate::agent_messages::",
|
"crate::agent_messages::",
|
||||||
"crate::agent_movement::",
|
"crate::agent_movement::",
|
||||||
|
"crate::asset_cache::",
|
||||||
|
"crate::asset_manager::",
|
||||||
|
"crate::asset_material::",
|
||||||
|
"crate::asset_models::",
|
||||||
"crate::byte_order::",
|
"crate::byte_order::",
|
||||||
"crate::attention_catalog::",
|
"crate::attention_catalog::",
|
||||||
"crate::binary::",
|
"crate::binary::",
|
||||||
@@ -998,6 +1002,7 @@ def validate_generated_shims() -> None:
|
|||||||
"crate::inventory_manager::",
|
"crate::inventory_manager::",
|
||||||
"crate::message_codec::",
|
"crate::message_codec::",
|
||||||
"crate::message_decoder::",
|
"crate::message_decoder::",
|
||||||
|
"crate::object_material::",
|
||||||
"crate::notation::",
|
"crate::notation::",
|
||||||
"crate::packet_catalog::",
|
"crate::packet_catalog::",
|
||||||
"crate::packet_wire::",
|
"crate::packet_wire::",
|
||||||
@@ -1006,6 +1011,7 @@ def validate_generated_shims() -> None:
|
|||||||
"crate::protobuf::",
|
"crate::protobuf::",
|
||||||
"crate::skeleton::",
|
"crate::skeleton::",
|
||||||
"crate::visual_catalog::",
|
"crate::visual_catalog::",
|
||||||
|
"crate::transfers::",
|
||||||
"crate::xml_codec::",
|
"crate::xml_codec::",
|
||||||
"stored_visual_params",
|
"stored_visual_params",
|
||||||
"self.native_",
|
"self.native_",
|
||||||
|
|||||||
Reference in New Issue
Block a user