Implement native archive and model workflows (#74)
Some checks failed
Native code generation / deterministic (push) Failing after 1m48s
Imaging and meshing gate / native (push) Successful in 5m26s
JPEG 2000 feature / linux (push) Successful in 2m51s
Native Rust workspace compile / compile (push) Successful in 5m28s
Skia feature / linux (push) Successful in 31m53s
Some checks failed
Native code generation / deterministic (push) Failing after 1m48s
Imaging and meshing gate / native (push) Successful in 5m26s
JPEG 2000 feature / linux (push) Successful in 2m51s
Native Rust workspace compile / compile (push) Successful in 5m28s
Skia feature / linux (push) Successful in 31m53s
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -852,6 +852,7 @@ dependencies = [
|
|||||||
name = "libremetaverse"
|
name = "libremetaverse"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"bcdec_rs",
|
"bcdec_rs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -864,6 +865,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tar",
|
||||||
"tokio",
|
"tokio",
|
||||||
"vorbis_rs",
|
"vorbis_rs",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -365,6 +365,11 @@ deduplicated HTTP subscribers, and atomic size-pruned disk caching. The ownershi
|
|||||||
correlation, and corruption contracts are documented in
|
correlation, and corruption contracts are documented in
|
||||||
[`docs/assets.md`](docs/assets.md).
|
[`docs/assets.md`](docs/assets.md).
|
||||||
|
|
||||||
|
Deterministic OAR/tar IO, bounded GLTF/GLB and Collada conversion, material
|
||||||
|
resolution, and opt-in mesh pricing/upload now use the same native asset and
|
||||||
|
capability layers. Their archive safety, offline determinism, and live-upload
|
||||||
|
boundaries are also 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: 369 types / 16,774 members; remaining surface is callable failure-only shims |
|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 401 types / 17,023 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 |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dds-bc67 = ["dep:bcdec_rs"]
|
|||||||
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
bcdec_rs = { version = "0.2.0", optional = true }
|
bcdec_rs = { version = "0.2.0", optional = true }
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.2"
|
||||||
futures-channel = "0.3.31"
|
futures-channel = "0.3.31"
|
||||||
@@ -25,6 +26,7 @@ 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"
|
serde_json = "1.0.143"
|
||||||
|
tar = "0.4.46"
|
||||||
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"
|
vorbis_rs = "0.5.5"
|
||||||
|
|
||||||
|
|||||||
1261
crates/libremetaverse/src/asset_archive.rs
Normal file
1261
crates/libremetaverse/src/asset_archive.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2599
crates/libremetaverse/src/gltf.rs
Normal file
2599
crates/libremetaverse/src/gltf.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ mod animesh_runtime;
|
|||||||
mod animesh_skinning;
|
mod animesh_skinning;
|
||||||
mod appearance_baker;
|
mod appearance_baker;
|
||||||
mod appearance_manager;
|
mod appearance_manager;
|
||||||
|
mod asset_archive;
|
||||||
mod asset_cache;
|
mod asset_cache;
|
||||||
mod asset_manager;
|
mod asset_manager;
|
||||||
mod asset_material;
|
mod asset_material;
|
||||||
@@ -31,6 +32,7 @@ mod estate_tools;
|
|||||||
mod event_queue;
|
mod event_queue;
|
||||||
mod friends_manager;
|
mod friends_manager;
|
||||||
mod gesture;
|
mod gesture;
|
||||||
|
mod gltf;
|
||||||
mod group_manager;
|
mod group_manager;
|
||||||
mod initial_outfit;
|
mod initial_outfit;
|
||||||
mod inventory;
|
mod inventory;
|
||||||
@@ -47,6 +49,7 @@ mod login;
|
|||||||
mod marketplace_runtime;
|
mod marketplace_runtime;
|
||||||
mod message_codec;
|
mod message_codec;
|
||||||
mod message_decoder;
|
mod message_decoder;
|
||||||
|
mod model_workflow;
|
||||||
mod network_manager;
|
mod network_manager;
|
||||||
mod object_manager;
|
mod object_manager;
|
||||||
mod object_material;
|
mod object_material;
|
||||||
@@ -93,17 +96,15 @@ type Terrain = Vec<Vec<f32>>;
|
|||||||
type TerrainLoaded = Box<dyn Fn(Terrain, i64, i64) + Send + Sync>;
|
type TerrainLoaded = Box<dyn Fn(Terrain, i64, i64) + Send + Sync>;
|
||||||
|
|
||||||
impl assets::OarFile {
|
impl assets::OarFile {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code, clippy::needless_pass_by_value)]
|
||||||
fn load_terrain(
|
fn load_terrain(
|
||||||
_file_path: &str,
|
file_path: &str,
|
||||||
_data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
_callback: TerrainLoaded,
|
callback: TerrainLoaded,
|
||||||
_bytes_read: i64,
|
bytes_read: i64,
|
||||||
_total_bytes: i64,
|
total_bytes: i64,
|
||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::asset_archive::load_terrain(file_path, &data, callback, bytes_read, total_bytes)
|
||||||
"M:LibreMetaverse.Assets.OarFile.LoadTerrain(System.String,System.Byte[],LibreMetaverse.Assets.OarFile.TerrainLoadedCallback,System.Int64,System.Int64)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1194
crates/libremetaverse/src/model_workflow.rs
Normal file
1194
crates/libremetaverse/src/model_workflow.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,29 @@ task material uploads use their two-stage capability contracts and require a
|
|||||||
complete response containing the new asset UUID. Upload size, cancellation, and
|
complete response containing the new asset UUID. Upload size, cancellation, and
|
||||||
timeout failures remove their pending correlation state.
|
timeout failures remove their pending correlation state.
|
||||||
|
|
||||||
|
OAR and model workflows are native as well. The tar reader validates checksums,
|
||||||
|
ustar paths, entry kinds, offsets, counts, nesting depth, per-entry size, and
|
||||||
|
expanded size before exposing data. OAR output is sorted and uses fixed gzip and
|
||||||
|
tar metadata, so identical asset, object, terrain, parcel, and settings trees
|
||||||
|
produce identical bytes. Loading recognizes canonical `UUID_type.ext` asset
|
||||||
|
names, bounded RAW32 terrain, and typed region-settings XML. Archive paths never
|
||||||
|
escape their selected root, and symbolic or hard links are rejected.
|
||||||
|
|
||||||
|
`GltfDocument` accepts bounded JSON glTF 2.0 and GLB 2.0 documents, resolves data
|
||||||
|
URIs or caller-provided external buffers, validates buffer-view/accessor ranges,
|
||||||
|
and decodes normalized scalar, vector, matrix, skin, and animation data. JSON and
|
||||||
|
GLB output retain scenes, nodes, surfaces, materials, textures, skins, and
|
||||||
|
animations in stable order; multi-buffer GLB output aligns and rebases views.
|
||||||
|
Collada conversion applies declared units and up-axis orientation, resolves
|
||||||
|
profile-COMMON effects and material bindings, triangulates triangles/quads, and
|
||||||
|
confines referenced textures to the document directory. Mesh assets use stable
|
||||||
|
quantization and LLSD section ordering.
|
||||||
|
|
||||||
|
Model upload is deliberately opt-in. Offline resource/pricing payload generation
|
||||||
|
does no network I/O; live preparation discovers the completed capability and
|
||||||
|
inventory layers, includes real destination folders, permission masks, and the
|
||||||
|
configured cost, then honors cancellation through both pricing and upload calls.
|
||||||
|
|
||||||
The disk cache uses UUID-only filenames by default and confines custom filename
|
The disk cache uses UUID-only filenames by default and confines custom filename
|
||||||
callbacks to the configured cache directory. Reads reject empty, oversized, or
|
callbacks to the configured cache directory. Reads reject empty, oversized, or
|
||||||
non-regular files. Writes use uniquely named same-directory files, flush them,
|
non-regular files. Writes use uniquely named same-directory files, flush them,
|
||||||
|
|||||||
52
tests/api-compile/Cargo.lock
generated
52
tests/api-compile/Cargo.lock
generated
@@ -305,6 +305,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
@@ -734,6 +744,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|||||||
name = "libremetaverse"
|
name = "libremetaverse"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"bcdec_rs",
|
"bcdec_rs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -746,6 +757,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tar",
|
||||||
"tokio",
|
"tokio",
|
||||||
"vorbis_rs",
|
"vorbis_rs",
|
||||||
]
|
]
|
||||||
@@ -865,6 +877,12 @@ dependencies = [
|
|||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -1390,6 +1408,19 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.43"
|
version = "0.23.43"
|
||||||
@@ -1686,6 +1717,17 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -2174,6 +2216,16 @@ version = "0.6.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|||||||
288
tests/compat/tests/archive_model_semantics.rs
Normal file
288
tests/compat/tests/archive_model_semantics.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use libremetaverse::GridClient;
|
||||||
|
use libremetaverse::assets::{
|
||||||
|
OarFile, OarFileAssetLoadedCallback, OarFileSceneObjectLoadedCallback,
|
||||||
|
OarFileSettingsLoadedCallback, OarFileTerrainLoadedCallback, RegionSettings,
|
||||||
|
};
|
||||||
|
use libremetaverse::import_export::{ColladaLoader, ModelFace, ModelPrim, ModelUploader};
|
||||||
|
use libremetaverse::rendering::Vertex;
|
||||||
|
use libremetaverse_structured_data::OSD;
|
||||||
|
use libremetaverse_types::{Quaternion, UUID, Vector2, Vector3};
|
||||||
|
|
||||||
|
fn temporary_directory(label: &str) -> PathBuf {
|
||||||
|
let id = UUID::random().expect("temporary UUID").to_string();
|
||||||
|
let path = std::env::temp_dir().join(format!("metacrate-{label}-{id}"));
|
||||||
|
fs::create_dir_all(&path).expect("create temporary directory");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oar_packaging_is_deterministic_and_sorted() {
|
||||||
|
let root = temporary_directory("oar");
|
||||||
|
for directory in ["assets", "objects", "terrains", "landdata", "settings"] {
|
||||||
|
fs::create_dir_all(root.join(directory)).expect("create OAR directory");
|
||||||
|
}
|
||||||
|
fs::write(root.join("assets/z.texture"), b"z").expect("write z asset");
|
||||||
|
fs::write(root.join("assets/a.texture"), b"a").expect("write a asset");
|
||||||
|
fs::write(root.join("terrains/region.r32"), 1.0_f32.to_le_bytes()).expect("write terrain");
|
||||||
|
let first = root.join("first.oar");
|
||||||
|
let second = root.join("second.oar");
|
||||||
|
OarFile::package_archive(
|
||||||
|
root.to_string_lossy().into_owned(),
|
||||||
|
first.to_string_lossy().into_owned(),
|
||||||
|
)
|
||||||
|
.expect("first package");
|
||||||
|
OarFile::package_archive(
|
||||||
|
root.to_string_lossy().into_owned(),
|
||||||
|
second.to_string_lossy().into_owned(),
|
||||||
|
)
|
||||||
|
.expect("second package");
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(first).expect("first bytes"),
|
||||||
|
fs::read(second).expect("second bytes")
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(root).expect("remove temporary OAR directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oar_canonical_asset_and_settings_names_round_trip() {
|
||||||
|
let root = temporary_directory("oar-roundtrip");
|
||||||
|
for directory in ["assets", "objects", "terrains", "landdata", "settings"] {
|
||||||
|
fs::create_dir_all(root.join(directory)).expect("create OAR directory");
|
||||||
|
}
|
||||||
|
let asset_id = UUID::random().expect("asset UUID");
|
||||||
|
let asset_bytes = b"native-j2c-payload".to_vec();
|
||||||
|
fs::write(
|
||||||
|
root.join(format!("assets/{asset_id}_texture.jp2")),
|
||||||
|
&asset_bytes,
|
||||||
|
)
|
||||||
|
.expect("write canonical asset");
|
||||||
|
let mut settings = RegionSettings::new().expect("region settings");
|
||||||
|
settings.allow_damage = true;
|
||||||
|
settings.water_height = 23.5;
|
||||||
|
settings
|
||||||
|
.to_xml(
|
||||||
|
root.join("settings/Native Region.xml")
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
.expect("write settings");
|
||||||
|
let archive = root.join("roundtrip.oar");
|
||||||
|
OarFile::package_archive(
|
||||||
|
root.to_string_lossy().into_owned(),
|
||||||
|
archive.to_string_lossy().into_owned(),
|
||||||
|
)
|
||||||
|
.expect("package round trip");
|
||||||
|
|
||||||
|
let loaded_asset = Arc::new(Mutex::new(None));
|
||||||
|
let loaded_asset_out = Arc::clone(&loaded_asset);
|
||||||
|
let loaded_settings = Arc::new(Mutex::new(None));
|
||||||
|
let loaded_settings_out = Arc::clone(&loaded_settings);
|
||||||
|
OarFile::unpackage_archive(
|
||||||
|
archive.to_string_lossy().into_owned(),
|
||||||
|
OarFileAssetLoadedCallback::from_callback(move |asset, _, _| {
|
||||||
|
*loaded_asset_out.lock().expect("asset lock") =
|
||||||
|
Some((asset.asset_id(), asset.asset_type(), asset.asset_data));
|
||||||
|
}),
|
||||||
|
OarFileTerrainLoadedCallback::from_callback(|_, _, _| {}),
|
||||||
|
OarFileSceneObjectLoadedCallback::from_callback(|_, _, _| {}),
|
||||||
|
OarFileSettingsLoadedCallback::from_callback(move |name, settings| {
|
||||||
|
*loaded_settings_out.lock().expect("settings lock") =
|
||||||
|
Some((name, settings.allow_damage, settings.water_height));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("unpackage round trip");
|
||||||
|
|
||||||
|
let asset = loaded_asset.lock().expect("loaded asset lock").clone();
|
||||||
|
assert_eq!(
|
||||||
|
asset,
|
||||||
|
Some((
|
||||||
|
asset_id,
|
||||||
|
libremetaverse_types::AssetType::Texture,
|
||||||
|
asset_bytes
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded_settings
|
||||||
|
.lock()
|
||||||
|
.expect("loaded settings lock")
|
||||||
|
.clone(),
|
||||||
|
Some(("Native Region".to_owned(), true, 23.5))
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(root).expect("remove round-trip directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_face_deduplicates_vertices_and_asset_is_deterministic() {
|
||||||
|
let vertex = || Vertex {
|
||||||
|
position: Vector3 {
|
||||||
|
x: -0.5,
|
||||||
|
y: 0.25,
|
||||||
|
z: 0.5,
|
||||||
|
},
|
||||||
|
normal: Vector3 {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 1.0,
|
||||||
|
},
|
||||||
|
tex_coord: Vector2 { x: 0.0, y: 1.0 },
|
||||||
|
};
|
||||||
|
let mut face = ModelFace::new().expect("model face");
|
||||||
|
face.add_vertex(vertex()).expect("first vertex");
|
||||||
|
face.add_vertex(vertex()).expect("duplicate vertex");
|
||||||
|
assert_eq!(face.vertices.len(), 1);
|
||||||
|
assert_eq!(face.indices, [0, 0]);
|
||||||
|
|
||||||
|
let mut prim = ModelPrim::new().expect("model prim");
|
||||||
|
prim.faces.push(face);
|
||||||
|
prim.create_asset(UUID::zero()).expect("first model asset");
|
||||||
|
let first = prim.asset.clone();
|
||||||
|
prim.create_asset(UUID::zero()).expect("second model asset");
|
||||||
|
assert!(!first.is_empty());
|
||||||
|
assert_eq!(prim.asset, first);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_upload_resources_are_deterministic_and_offline() {
|
||||||
|
let mut face = ModelFace::new().expect("model face");
|
||||||
|
face.material.texture = "surface.jp2".into();
|
||||||
|
face.material.texture_data = vec![1, 2, 3, 4];
|
||||||
|
for position in [
|
||||||
|
Vector3::zero(),
|
||||||
|
Vector3 {
|
||||||
|
x: 1.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0,
|
||||||
|
},
|
||||||
|
Vector3 {
|
||||||
|
x: 0.0,
|
||||||
|
y: 1.0,
|
||||||
|
z: 0.0,
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
face.add_vertex(Vertex {
|
||||||
|
position,
|
||||||
|
normal: Vector3 {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 1.0,
|
||||||
|
},
|
||||||
|
tex_coord: Vector2::zero(),
|
||||||
|
})
|
||||||
|
.expect("upload vertex");
|
||||||
|
}
|
||||||
|
let mut prim = ModelPrim::new().expect("model prim");
|
||||||
|
prim.faces.push(face);
|
||||||
|
prim.create_asset(UUID::zero()).expect("mesh asset");
|
||||||
|
let uploader = ModelUploader::new(
|
||||||
|
GridClient::new().expect("offline client"),
|
||||||
|
vec![prim],
|
||||||
|
"Offline mesh".into(),
|
||||||
|
"No network required".into(),
|
||||||
|
)
|
||||||
|
.expect("model uploader");
|
||||||
|
let first = uploader.asset_resources(false).expect("offline resources");
|
||||||
|
let second = uploader.asset_resources(false).expect("repeat resources");
|
||||||
|
assert_eq!(first, second);
|
||||||
|
let OSD::Map(map) = first else {
|
||||||
|
panic!("resource map");
|
||||||
|
};
|
||||||
|
let OSD::Array(textures) = map.get("texture_list").expect("texture list") else {
|
||||||
|
panic!("texture array");
|
||||||
|
};
|
||||||
|
assert_eq!(textures, &[OSD::Binary(Vec::new())]);
|
||||||
|
let OSD::Map(upload) = uploader.asset_resources(true).expect("upload resources") else {
|
||||||
|
panic!("upload map");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
upload.get("texture_list"),
|
||||||
|
Some(&OSD::Array(vec![OSD::Binary(vec![1, 2, 3, 4])]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collada_triangle_converts_to_native_model_asset() {
|
||||||
|
let root = temporary_directory("collada");
|
||||||
|
let file = root.join("triangle.dae");
|
||||||
|
fs::write(
|
||||||
|
&file,
|
||||||
|
r##"<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
|
||||||
|
<library_geometries><geometry id="triangle"><mesh>
|
||||||
|
<source id="positions"><float_array id="positions-array" count="9">0 0 0 1 0 0 0 1 0</float_array>
|
||||||
|
<technique_common><accessor source="#positions-array" count="3" stride="3"/></technique_common></source>
|
||||||
|
<vertices id="vertices"><input semantic="POSITION" source="#positions"/></vertices>
|
||||||
|
<triangles count="1"><input semantic="VERTEX" source="#vertices" offset="0"/><p>0 1 2</p></triangles>
|
||||||
|
</mesh></geometry></library_geometries>
|
||||||
|
</COLLADA>"##,
|
||||||
|
)
|
||||||
|
.expect("write Collada fixture");
|
||||||
|
let prims = ColladaLoader::new(None)
|
||||||
|
.expect("Collada loader")
|
||||||
|
.load(file.to_string_lossy().into_owned(), false)
|
||||||
|
.expect("load Collada");
|
||||||
|
assert_eq!(prims.len(), 1);
|
||||||
|
assert_eq!(prims[0].faces.len(), 1);
|
||||||
|
assert_eq!(prims[0].faces[0].indices, [0, 1, 2]);
|
||||||
|
assert!(!prims[0].asset.is_empty());
|
||||||
|
assert_eq!(prims[0].rotation, Quaternion::identity());
|
||||||
|
fs::remove_dir_all(root).expect("remove temporary Collada directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collada_applies_units_effects_and_instance_material_bindings() {
|
||||||
|
let root = temporary_directory("collada-material");
|
||||||
|
let file = root.join("material.dae");
|
||||||
|
fs::write(
|
||||||
|
&file,
|
||||||
|
r##"<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema">
|
||||||
|
<asset><unit meter="2"/><up_axis>Z_UP</up_axis></asset>
|
||||||
|
<library_effects><effect id="green"><profile_COMMON><technique sid="common"><lambert>
|
||||||
|
<diffuse><color>0.1 0.8 0.2 1</color></diffuse>
|
||||||
|
</lambert></technique></profile_COMMON></effect></library_effects>
|
||||||
|
<library_materials><material id="surface"><instance_effect url="#green"/></material></library_materials>
|
||||||
|
<library_geometries><geometry id="triangle"><mesh>
|
||||||
|
<source id="positions"><float_array>0 0 0 1 0 0 0 1 0</float_array>
|
||||||
|
<technique_common><accessor stride="3"/></technique_common></source>
|
||||||
|
<vertices id="vertices"><input semantic="POSITION" source="#positions"/></vertices>
|
||||||
|
<triangles count="1" material="slot"><input semantic="VERTEX" source="#vertices" offset="0"/><p>0 1 2</p></triangles>
|
||||||
|
</mesh></geometry></library_geometries>
|
||||||
|
<library_visual_scenes><visual_scene id="scene"><node id="instance"><instance_geometry url="#triangle">
|
||||||
|
<bind_material><technique_common><instance_material symbol="slot" target="#surface"/></technique_common></bind_material>
|
||||||
|
</instance_geometry></node></visual_scene></library_visual_scenes>
|
||||||
|
</COLLADA>"##,
|
||||||
|
)
|
||||||
|
.expect("write material Collada fixture");
|
||||||
|
let prims = ColladaLoader::new(None)
|
||||||
|
.expect("Collada loader")
|
||||||
|
.load(file.to_string_lossy().into_owned(), false)
|
||||||
|
.expect("load material Collada");
|
||||||
|
assert_eq!(prims.len(), 1);
|
||||||
|
assert_eq!(prims[0].scale.x, 2.0);
|
||||||
|
assert_eq!(prims[0].scale.y, 2.0);
|
||||||
|
assert_eq!(prims[0].faces[0].material.id, "surface");
|
||||||
|
assert_eq!(prims[0].faces[0].material.diffuse_color.g, 0.8);
|
||||||
|
fs::remove_dir_all(root).expect("remove material Collada directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collada_texture_path_cannot_escape_document_root() {
|
||||||
|
let root = temporary_directory("collada-traversal");
|
||||||
|
let file = root.join("traversal.dae");
|
||||||
|
fs::write(
|
||||||
|
&file,
|
||||||
|
r##"<COLLADA><library_images><image id="escape"><init_from>../secret.jp2</init_from></image></library_images>
|
||||||
|
<library_materials><material id="mat"><diffuse><texture texture="escape"/></diffuse></material></library_materials></COLLADA>"##,
|
||||||
|
)
|
||||||
|
.expect("write traversal fixture");
|
||||||
|
assert!(
|
||||||
|
ColladaLoader::new(None)
|
||||||
|
.expect("Collada loader")
|
||||||
|
.load(file.to_string_lossy().into_owned(), true)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(root).expect("remove traversal directory");
|
||||||
|
}
|
||||||
@@ -515,3 +515,67 @@ fn gltf_node_explicit_matrix() {
|
|||||||
assert_close(matrix.m24, 6.0, 1e-5);
|
assert_close(matrix.m24, 6.0, 1e-5);
|
||||||
assert_close(matrix.m34, 7.0, 1e-5);
|
assert_close(matrix.m34, 7.0, 1e-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gltf_rejects_buffer_view_outside_declared_buffer() {
|
||||||
|
let json = r#"{
|
||||||
|
"asset":{"version":"2.0"},
|
||||||
|
"buffers":[{"byteLength":4,"uri":"data:application/octet-stream;base64,AAAAAA=="}],
|
||||||
|
"bufferViews":[{"buffer":0,"byteOffset":3,"byteLength":4}]
|
||||||
|
}"#;
|
||||||
|
assert!(GltfDocument::load_gltf(json.into(), None).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gltf_rejects_cyclic_node_graphs() {
|
||||||
|
let json = r#"{
|
||||||
|
"asset":{"version":"2.0"},
|
||||||
|
"nodes":[{"children":[1]},{"children":[0]}],
|
||||||
|
"scenes":[{"nodes":[0]}],
|
||||||
|
"scene":0
|
||||||
|
}"#;
|
||||||
|
assert!(GltfDocument::load_gltf(json.into(), None).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gltf_surface_skin_and_animation_round_trip() {
|
||||||
|
let json = r#"{
|
||||||
|
"asset":{"version":"2.0"},
|
||||||
|
"accessors":[
|
||||||
|
{"componentType":5126,"count":0,"type":"SCALAR"},
|
||||||
|
{"componentType":5126,"count":0,"type":"VEC3"}
|
||||||
|
],
|
||||||
|
"images":[{"uri":"data:image/png;base64,iVBORw0KGgo=","mimeType":"image/png","name":"image"}],
|
||||||
|
"samplers":[{"magFilter":9729,"minFilter":9987,"wrapS":10497,"wrapT":33071,"name":"sampler"}],
|
||||||
|
"textures":[{"sampler":0,"source":0,"name":"texture"}],
|
||||||
|
"materials":[{
|
||||||
|
"name":"surface",
|
||||||
|
"pbrMetallicRoughness":{"baseColorTexture":{"index":0},"metallicRoughnessTexture":{"index":0}},
|
||||||
|
"normalTexture":{"index":0,"scale":0.5},
|
||||||
|
"occlusionTexture":{"index":0,"strength":0.75},
|
||||||
|
"emissiveTexture":{"index":0},
|
||||||
|
"emissiveFactor":[0.1,0.2,0.3]
|
||||||
|
}],
|
||||||
|
"nodes":[{"skin":0}],
|
||||||
|
"skins":[{"joints":[0],"skeleton":0,"name":"skin"}],
|
||||||
|
"animations":[{
|
||||||
|
"name":"move",
|
||||||
|
"samplers":[{"input":0,"output":1,"interpolation":"LINEAR"}],
|
||||||
|
"channels":[{"sampler":0,"target":{"node":0,"path":"translation"}}]
|
||||||
|
}]
|
||||||
|
}"#;
|
||||||
|
let document = GltfDocument::load_gltf(json.into(), None).expect("load complete surface");
|
||||||
|
let serialized = document.to_json(None).expect("serialize complete surface");
|
||||||
|
let round_trip = GltfDocument::load_gltf(serialized, None).expect("reload complete surface");
|
||||||
|
assert_eq!(round_trip.images().len(), 1);
|
||||||
|
assert_eq!(round_trip.samplers().len(), 1);
|
||||||
|
assert_eq!(round_trip.textures().len(), 1);
|
||||||
|
assert_eq!(round_trip.skins()[0].joints(), [0]);
|
||||||
|
assert_eq!(round_trip.animations()[0].channels().len(), 1);
|
||||||
|
let material = &round_trip.materials()[0];
|
||||||
|
assert!(material.base_color_texture().is_some());
|
||||||
|
assert!(material.metallic_roughness_texture().is_some());
|
||||||
|
assert!(material.normal_texture().is_some());
|
||||||
|
assert!(material.occlusion_texture().is_some());
|
||||||
|
assert!(material.emissive_texture().is_some());
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,16 +6,8 @@ use libremetaverse::messages::linden::AgentDropGroupMessage;
|
|||||||
use libremetaverse::packets::UseCircuitCodePacket;
|
use libremetaverse::packets::UseCircuitCodePacket;
|
||||||
use libremetaverse::{BitPack, PacketFrequency, PermissionMask, Permissions, Primitive};
|
use libremetaverse::{BitPack, PacketFrequency, PermissionMask, Permissions, Primitive};
|
||||||
|
|
||||||
fn member_id<T>(result: Result<T, libremetaverse::Error>) -> &'static str {
|
|
||||||
result
|
|
||||||
.err()
|
|
||||||
.expect("failure-only shim unexpectedly returned a value")
|
|
||||||
.csharp_member()
|
|
||||||
.expect("failure-only shim error")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn each_wire_data_domain_is_callable_and_failure_only() {
|
fn each_wire_data_domain_is_callable_at_its_native_boundary() {
|
||||||
let packet = UseCircuitCodePacket::new_with_constructor()
|
let packet = UseCircuitCodePacket::new_with_constructor()
|
||||||
.expect("generated packet constructor is native");
|
.expect("generated packet constructor is native");
|
||||||
assert_eq!(packet.circuit_code.code, 0);
|
assert_eq!(packet.circuit_code.code, 0);
|
||||||
@@ -26,20 +18,26 @@ fn each_wire_data_domain_is_callable_and_failure_only() {
|
|||||||
.is_empty()
|
.is_empty()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
member_id(AssetTexture::new_with_constructor()),
|
AssetTexture::new_with_constructor()
|
||||||
"M:LibreMetaverse.Assets.AssetTexture.#ctor"
|
.expect("native asset texture constructor")
|
||||||
|
.components,
|
||||||
|
0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
member_id(GltfDocument::new()),
|
GltfDocument::new()
|
||||||
"M:LibreMetaverse.Assets.Gltf.GltfDocument.#ctor"
|
.expect("native glTF constructor")
|
||||||
|
.version(),
|
||||||
|
"2.0"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ModelPrim::new()
|
||||||
|
.expect("native model constructor")
|
||||||
|
.asset
|
||||||
|
.is_empty()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
member_id(ModelPrim::new()),
|
OarFile::package_archive(String::new(), String::new()),
|
||||||
"M:LibreMetaverse.ImportExport.ModelPrim.#ctor"
|
Err(libremetaverse::Error::InvalidOperation)
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
member_id(OarFile::package_archive(String::new(), String::new())),
|
|
||||||
"M:LibreMetaverse.Assets.OarFile.PackageArchive(System.String,System.String)"
|
|
||||||
);
|
);
|
||||||
let bit_pack = BitPack::new(Vec::new(), 0).expect("native bit pack constructor");
|
let bit_pack = BitPack::new(Vec::new(), 0).expect("native bit pack constructor");
|
||||||
assert_eq!(bit_pack.byte_pos(), 0);
|
assert_eq!(bit_pack.byte_pos(), 0);
|
||||||
|
|||||||
@@ -140,6 +140,38 @@ NATIVE_TYPES = {
|
|||||||
"T:LibreMetaverse.Assets.AssetTexture": "crate::asset_models::AssetTexture",
|
"T:LibreMetaverse.Assets.AssetTexture": "crate::asset_models::AssetTexture",
|
||||||
"T:LibreMetaverse.Assets.AssetWearable": "crate::asset_models::AssetWearable",
|
"T:LibreMetaverse.Assets.AssetWearable": "crate::asset_models::AssetWearable",
|
||||||
"T:LibreMetaverse.Assets.GltfTextureTransform": "crate::asset_material::GltfTextureTransform",
|
"T:LibreMetaverse.Assets.GltfTextureTransform": "crate::asset_material::GltfTextureTransform",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfAccessor": "crate::gltf::GltfAccessor",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfAnimation": "crate::gltf::GltfAnimation",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfAnimationChannel": "crate::gltf::GltfAnimationChannel",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfAnimationChannelTarget": "crate::gltf::GltfAnimationChannelTarget",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfAnimationSampler": "crate::gltf::GltfAnimationSampler",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfBuffer": "crate::gltf::GltfBuffer",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfBufferView": "crate::gltf::GltfBufferView",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfDocument": "crate::gltf::GltfDocument",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfDocumentMaterial": "crate::gltf::GltfDocumentMaterial",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfImage": "crate::gltf::GltfImage",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfMesh": "crate::gltf::GltfMesh",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfNode": "crate::gltf::GltfNode",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfNormalTextureRef": "crate::gltf::GltfNormalTextureRef",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfOcclusionTextureRef": "crate::gltf::GltfOcclusionTextureRef",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfPrimitive": "crate::gltf::GltfPrimitive",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfSampler": "crate::gltf::GltfSampler",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfScene": "crate::gltf::GltfScene",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfSkin": "crate::gltf::GltfSkin",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfTexture": "crate::gltf::GltfTexture",
|
||||||
|
"T:LibreMetaverse.Assets.Gltf.GltfTextureRef": "crate::gltf::GltfTextureRef",
|
||||||
|
"T:LibreMetaverse.Assets.TarArchiveReader": "crate::asset_archive::TarArchiveReader",
|
||||||
|
"T:LibreMetaverse.Assets.TarArchiveWriter": "crate::asset_archive::TarArchiveWriter",
|
||||||
|
"T:LibreMetaverse.Assets.OarFile.AssetLoadedCallback": "crate::asset_archive::OarFileAssetLoadedCallback",
|
||||||
|
"T:LibreMetaverse.Assets.OarFile.SceneObjectLoadedCallback": "crate::asset_archive::OarFileSceneObjectLoadedCallback",
|
||||||
|
"T:LibreMetaverse.Assets.OarFile.SettingsLoadedCallback": "crate::asset_archive::OarFileSettingsLoadedCallback",
|
||||||
|
"T:LibreMetaverse.Assets.OarFile.TerrainLoadedCallback": "crate::asset_archive::OarFileTerrainLoadedCallback",
|
||||||
|
"T:LibreMetaverse.Assets.RegionSettings": "crate::asset_archive::RegionSettings",
|
||||||
|
"T:LibreMetaverse.ImportExport.ColladaLoader": "crate::model_workflow::ColladaLoader",
|
||||||
|
"T:LibreMetaverse.ImportExport.ModelFace": "crate::model_workflow::ModelFace",
|
||||||
|
"T:LibreMetaverse.ImportExport.ModelMaterial": "crate::model_workflow::ModelMaterial",
|
||||||
|
"T:LibreMetaverse.ImportExport.ModelPrim": "crate::model_workflow::ModelPrim",
|
||||||
|
"T:LibreMetaverse.ImportExport.ModelUploader": "crate::model_workflow::ModelUploader",
|
||||||
"T:LibreMetaverse.GLTFMaterialUpdate": "crate::object_material::GLTFMaterialUpdate",
|
"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",
|
||||||
@@ -474,6 +506,25 @@ NATIVE_DECLARATIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NATIVE_MEMBER_BODIES = {
|
NATIVE_MEMBER_BODIES = {
|
||||||
|
"F:LibreMetaverse.Assets.ArchiveConstants.ASSET_TYPE_TO_EXTENSION":
|
||||||
|
"crate::asset_archive::asset_type_to_extension()",
|
||||||
|
"F:LibreMetaverse.Assets.ArchiveConstants.EXTENSION_TO_ASSET_TYPE":
|
||||||
|
"crate::asset_archive::extension_to_asset_type()",
|
||||||
|
"M:LibreMetaverse.Assets.ArchiveConstants.#ctor": "Ok(Self)",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.ClearAssetFolder(System.String)":
|
||||||
|
"crate::asset_archive::clear_asset_folder(assets_path)",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.PackageArchive(System.String,System.String)":
|
||||||
|
"crate::asset_archive::package_archive(directory_name, filename)",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.SaveAssetsAsync(LibreMetaverse.AssetManager,LibreMetaverse.AssetType,System.Collections.Generic.IList{LibreMetaverse.UUID},System.String)":
|
||||||
|
"crate::asset_archive::save_assets(asset_manager, asset_type, assets, assets_path).await",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.SaveRegionSettings(LibreMetaverse.Simulator,System.String)":
|
||||||
|
"crate::asset_archive::save_region_settings(sim, settings_path)",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.SaveSimAssetsAsync(LibreMetaverse.AssetManager,LibreMetaverse.AssetType,LibreMetaverse.UUID,LibreMetaverse.UUID,LibreMetaverse.UUID,System.String)":
|
||||||
|
"crate::asset_archive::save_sim_asset(asset_manager, asset_type, asset_id, item_id, prim_id, assets_path).await",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.SaveTerrain(LibreMetaverse.Simulator,System.String)":
|
||||||
|
"crate::asset_archive::save_terrain(sim, terrain_path)",
|
||||||
|
"M:LibreMetaverse.Assets.OarFile.UnpackageArchive(System.String,LibreMetaverse.Assets.OarFile.AssetLoadedCallback,LibreMetaverse.Assets.OarFile.TerrainLoadedCallback,LibreMetaverse.Assets.OarFile.SceneObjectLoadedCallback,LibreMetaverse.Assets.OarFile.SettingsLoadedCallback)":
|
||||||
|
"crate::asset_archive::unpackage_archive(filename, asset_callback, terrain_callback, object_callback, settings_callback)",
|
||||||
"P:LibreMetaverse.GridClient.Estate": "self.native_estate().unwrap_or_else(|_| panic!(\"failed to construct EstateTools\"))",
|
"P:LibreMetaverse.GridClient.Estate": "self.native_estate().unwrap_or_else(|_| panic!(\"failed to construct EstateTools\"))",
|
||||||
"P:LibreMetaverse.GridClient.Estate#set": "self.native_set_estate(value)",
|
"P:LibreMetaverse.GridClient.Estate#set": "self.native_set_estate(value)",
|
||||||
"P:LibreMetaverse.GridClient.Environment":
|
"P:LibreMetaverse.GridClient.Environment":
|
||||||
|
|||||||
Reference in New Issue
Block a user