Fix bounded avatar appearance recovery after login
This commit is contained in:
@@ -3,6 +3,22 @@
|
||||
This file records intentional differences from the C# LibreMetaverse source. Preserve or
|
||||
re-evaluate these changes when importing a newer upstream version.
|
||||
|
||||
## Bounded login appearance recovery
|
||||
|
||||
- A successful login now consumes `AgentSettings.send_appearance`, initializes the shared
|
||||
`AppearanceManager`, and starts one generation-fenced appearance request after the simulator is
|
||||
ready.
|
||||
- Self `AvatarAppearance` acknowledgements are accepted as complete only when required baked
|
||||
texture IDs, visual parameters, and the expected COF version are present. Transient failures use
|
||||
bounded delayed forced rebakes; disconnects and simulator changes cancel stale work.
|
||||
- COF attachments are compared with simulator child prims by `AttachItemID`; only missing items are
|
||||
restored through the existing attachment API. Non-secret structured recovery events expose the
|
||||
bake path, counts, serial, retries, acknowledgement, and elapsed time.
|
||||
- Baked-texture capability responses are decoded as LLSD, as required by the simulator protocol,
|
||||
instead of incorrectly treating their uploader and completion maps as JSON.
|
||||
|
||||
Files: `src/network_manager.rs`, `src/appearance_manager.rs`, `src/asset_manager.rs`.
|
||||
|
||||
## OpenSim varregion dimensions
|
||||
|
||||
- `GridRegion` retains the optional `MapBlockReply.Size` dimensions. Missing or zero dimensions
|
||||
|
||||
@@ -29,13 +29,17 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub(crate) const ATTACHMENT_ADD: u8 = 0x80;
|
||||
const MAX_WEARABLE_LAYERS: usize = 60;
|
||||
const MAX_FOLDER_TRAVERSAL: usize = 256;
|
||||
const MAX_COF_ENTRIES: usize = 512;
|
||||
const APPEARANCE_RECOVERY_ATTEMPTS: u32 = 3;
|
||||
const APPEARANCE_ACK_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const ATTACHMENT_SETTLE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn mutex<T>(lock: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
lock.lock()
|
||||
@@ -136,6 +140,69 @@ struct AppearanceEvents {
|
||||
appearance_set: EventRegistry<AppearanceSetEventArgs>,
|
||||
cached_bakes_reply: EventRegistry<AgentCachedBakesReplyEventArgs>,
|
||||
rebake_avatar_requested: EventRegistry<RebakeAvatarTexturesEventArgs>,
|
||||
recovery: EventRegistry<AppearanceRecoveryEvent>,
|
||||
}
|
||||
|
||||
/// A bounded, non-secret appearance recovery phase suitable for diagnostics.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum AppearanceRecoveryPhase {
|
||||
LoginReady,
|
||||
OutfitReady,
|
||||
BakeComplete,
|
||||
AppearanceSent,
|
||||
AppearanceAcknowledged,
|
||||
AttachmentsRepaired,
|
||||
Retrying,
|
||||
Complete,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// The operation boundary most recently reached by an appearance request.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum AppearanceRecoveryStage {
|
||||
Starting,
|
||||
Outfit,
|
||||
Cache,
|
||||
Assets,
|
||||
Bake,
|
||||
Publish,
|
||||
Acknowledgement,
|
||||
Attachments,
|
||||
}
|
||||
|
||||
impl AppearanceRecoveryStage {
|
||||
const fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
1 => Self::Outfit,
|
||||
2 => Self::Cache,
|
||||
3 => Self::Assets,
|
||||
4 => Self::Bake,
|
||||
5 => Self::Publish,
|
||||
6 => Self::Acknowledgement,
|
||||
7 => Self::Attachments,
|
||||
_ => Self::Starting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Structured progress from automatic login appearance recovery.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct AppearanceRecoveryEvent {
|
||||
pub phase: AppearanceRecoveryPhase,
|
||||
pub stage: AppearanceRecoveryStage,
|
||||
pub generation: u64,
|
||||
pub retry: u32,
|
||||
pub elapsed_millis: u64,
|
||||
pub cof_version: i32,
|
||||
pub wearable_count: usize,
|
||||
pub attachment_count: usize,
|
||||
pub missing_attachment_count: usize,
|
||||
pub cached_bake_count: usize,
|
||||
pub server_baking: bool,
|
||||
pub appearance_serial: u64,
|
||||
pub acknowledged: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct AppearanceManagerInner {
|
||||
@@ -158,6 +225,13 @@ pub(crate) struct AppearanceManagerInner {
|
||||
appearance_serial: AtomicU64,
|
||||
appearance_cancel: Mutex<Option<CancellationTokenSource>>,
|
||||
appearance_gate: Arc<tokio::sync::Semaphore>,
|
||||
recovery_stage: AtomicU8,
|
||||
recovery_generation: AtomicU64,
|
||||
logged_in: AtomicBool,
|
||||
acknowledgement_sequence: AtomicU64,
|
||||
acknowledged_cof_version: AtomicI32,
|
||||
acknowledged_complete: AtomicBool,
|
||||
acknowledgement_notify: tokio::sync::Notify,
|
||||
events: AppearanceEvents,
|
||||
login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
|
||||
network_subscriptions: Mutex<Vec<Subscription>>,
|
||||
@@ -255,11 +329,35 @@ fn handle_avatar_appearance(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>)
|
||||
if packet.sender.id != network.native_agent_id() {
|
||||
return;
|
||||
}
|
||||
let expected_cof = inner.last_cof_version.load(Ordering::Acquire);
|
||||
if let Some(appearance) = packet.appearance_data.first() {
|
||||
inner
|
||||
.last_cof_version
|
||||
.fetch_max(appearance.cof_version, Ordering::AcqRel);
|
||||
}
|
||||
let cof_version = packet
|
||||
.appearance_data
|
||||
.first()
|
||||
.map_or(-1, |appearance| appearance.cof_version);
|
||||
let complete = cof_version >= expected_cof && complete_baked_appearance(&packet);
|
||||
inner
|
||||
.acknowledged_cof_version
|
||||
.store(cof_version, Ordering::Release);
|
||||
inner
|
||||
.acknowledged_complete
|
||||
.store(complete, Ordering::Release);
|
||||
inner
|
||||
.acknowledgement_sequence
|
||||
.fetch_add(1, Ordering::AcqRel);
|
||||
inner.acknowledgement_notify.notify_waiters();
|
||||
emit_recovery(
|
||||
inner,
|
||||
AppearanceRecoveryPhase::AppearanceAcknowledged,
|
||||
0,
|
||||
Instant::now(),
|
||||
inner.recovery_generation.load(Ordering::Acquire),
|
||||
complete,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_wearables_update(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
|
||||
@@ -365,6 +463,328 @@ fn spawn_appearance_request(inner: &Arc<AppearanceManagerInner>, force_rebake: b
|
||||
});
|
||||
}
|
||||
|
||||
fn install_recovery_lifecycle(
|
||||
inner: &Arc<AppearanceManagerInner>,
|
||||
client: &GridClient,
|
||||
) -> Result<(), Error> {
|
||||
let network = client.native_network()?;
|
||||
let weak = Arc::downgrade(inner);
|
||||
let login = network.native_subscribe_login_progress(Arc::new(move |event| {
|
||||
let Some(inner) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if event.status() == crate::LoginStatus::Success
|
||||
&& inner
|
||||
.client
|
||||
.upgrade()
|
||||
.is_some_and(|client| client.settings_ref().agent.send_appearance)
|
||||
&& !inner.logged_in.swap(true, Ordering::AcqRel)
|
||||
{
|
||||
start_recovery(&inner, "appearance-login");
|
||||
}
|
||||
}));
|
||||
let weak = Arc::downgrade(inner);
|
||||
let disconnected = network.native_subscribe_disconnected(Arc::new(move |_| {
|
||||
if let Some(inner) = weak.upgrade() {
|
||||
inner.logged_in.store(false, Ordering::Release);
|
||||
cancel_recovery(&inner);
|
||||
}
|
||||
}));
|
||||
let weak = Arc::downgrade(inner);
|
||||
let changed = network.native_subscribe_sim_changed(Arc::new(move |_| {
|
||||
if let Some(inner) = weak.upgrade()
|
||||
&& inner.logged_in.load(Ordering::Acquire)
|
||||
{
|
||||
start_recovery(&inner, "appearance-region-change");
|
||||
}
|
||||
}));
|
||||
mutex(&inner.network_subscriptions).extend([login, disconnected, changed]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_recovery(inner: &AppearanceManagerInner) {
|
||||
inner.recovery_generation.fetch_add(1, Ordering::AcqRel);
|
||||
if let Some(source) = mutex(&inner.appearance_cancel).take() {
|
||||
source.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
fn start_recovery(inner: &Arc<AppearanceManagerInner>, name: &str) {
|
||||
cancel_recovery(inner);
|
||||
let generation = inner.recovery_generation.load(Ordering::Acquire);
|
||||
let manager = AppearanceManager::native_from_inner(Arc::clone(inner));
|
||||
let _ = std::thread::Builder::new()
|
||||
.name(name.to_owned())
|
||||
.spawn(move || {
|
||||
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
runtime.block_on(run_recovery(manager, generation));
|
||||
});
|
||||
}
|
||||
|
||||
async fn run_recovery(manager: AppearanceManager, generation: u64) {
|
||||
let started = Instant::now();
|
||||
let mut requested_attachments = HashSet::new();
|
||||
emit_recovery(
|
||||
&manager.inner,
|
||||
AppearanceRecoveryPhase::LoginReady,
|
||||
0,
|
||||
started,
|
||||
generation,
|
||||
false,
|
||||
);
|
||||
for retry in 0..APPEARANCE_RECOVERY_ATTEMPTS {
|
||||
if !recovery_current(&manager.inner, generation) {
|
||||
emit_recovery(
|
||||
&manager.inner,
|
||||
AppearanceRecoveryPhase::Cancelled,
|
||||
retry,
|
||||
started,
|
||||
generation,
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if retry > 0 {
|
||||
emit_recovery(
|
||||
&manager.inner,
|
||||
AppearanceRecoveryPhase::Retrying,
|
||||
retry,
|
||||
started,
|
||||
generation,
|
||||
false,
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs((1_u64 << retry).saturating_sub(1))).await;
|
||||
}
|
||||
let acknowledgement = manager
|
||||
.inner
|
||||
.acknowledgement_sequence
|
||||
.load(Ordering::Acquire);
|
||||
if manager
|
||||
.native_request_set_appearance(retry != 0)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !recovery_current(&manager.inner, generation) {
|
||||
continue;
|
||||
}
|
||||
for phase in [
|
||||
AppearanceRecoveryPhase::OutfitReady,
|
||||
AppearanceRecoveryPhase::BakeComplete,
|
||||
AppearanceRecoveryPhase::AppearanceSent,
|
||||
] {
|
||||
emit_recovery(&manager.inner, phase, retry, started, generation, false);
|
||||
}
|
||||
set_recovery_stage(&manager.inner, AppearanceRecoveryStage::Attachments);
|
||||
let repaired = repair_missing_attachments(&manager, &mut requested_attachments);
|
||||
if repaired != 0 {
|
||||
emit_recovery(
|
||||
&manager.inner,
|
||||
AppearanceRecoveryPhase::AttachmentsRepaired,
|
||||
retry,
|
||||
started,
|
||||
generation,
|
||||
false,
|
||||
);
|
||||
}
|
||||
set_recovery_stage(&manager.inner, AppearanceRecoveryStage::Acknowledgement);
|
||||
if !wait_for_acknowledgement(&manager.inner, generation, acknowledgement).await {
|
||||
continue;
|
||||
}
|
||||
set_recovery_stage(&manager.inner, AppearanceRecoveryStage::Attachments);
|
||||
if wait_for_attachments(&manager.inner, generation).await {
|
||||
emit_recovery(
|
||||
&manager.inner,
|
||||
AppearanceRecoveryPhase::Complete,
|
||||
retry,
|
||||
started,
|
||||
generation,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit_recovery(
|
||||
&manager.inner,
|
||||
AppearanceRecoveryPhase::Failed,
|
||||
APPEARANCE_RECOVERY_ATTEMPTS,
|
||||
started,
|
||||
generation,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn recovery_current(inner: &AppearanceManagerInner, generation: u64) -> bool {
|
||||
!inner.disposed.load(Ordering::Acquire)
|
||||
&& inner.logged_in.load(Ordering::Acquire)
|
||||
&& inner.recovery_generation.load(Ordering::Acquire) == generation
|
||||
}
|
||||
|
||||
fn set_recovery_stage(inner: &AppearanceManagerInner, stage: AppearanceRecoveryStage) {
|
||||
inner.recovery_stage.store(stage as u8, Ordering::Release);
|
||||
}
|
||||
|
||||
async fn wait_for_acknowledgement(
|
||||
inner: &AppearanceManagerInner,
|
||||
generation: u64,
|
||||
previous_sequence: u64,
|
||||
) -> bool {
|
||||
tokio::time::timeout(APPEARANCE_ACK_TIMEOUT, async {
|
||||
loop {
|
||||
let notified = inner.acknowledgement_notify.notified();
|
||||
if inner.acknowledgement_sequence.load(Ordering::Acquire) > previous_sequence
|
||||
&& inner.acknowledged_complete.load(Ordering::Acquire)
|
||||
&& inner.acknowledged_cof_version.load(Ordering::Acquire)
|
||||
>= inner.last_cof_version.load(Ordering::Acquire)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if !recovery_current(inner, generation) {
|
||||
return false;
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn wait_for_attachments(inner: &AppearanceManagerInner, generation: u64) -> bool {
|
||||
let deadline = tokio::time::Instant::now() + ATTACHMENT_SETTLE_TIMEOUT;
|
||||
loop {
|
||||
if missing_attachments(inner).is_empty() {
|
||||
return true;
|
||||
}
|
||||
if !recovery_current(inner, generation) || tokio::time::Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn repair_missing_attachments(manager: &AppearanceManager, requested: &mut HashSet<UUID>) -> usize {
|
||||
let missing = missing_attachments(&manager.inner);
|
||||
let mut repaired = 0;
|
||||
for attachment in missing {
|
||||
let item_id = attachment.item.base.uuid();
|
||||
if requested.insert(item_id) {
|
||||
if manager
|
||||
.native_attach(attachment.item, attachment.point, false)
|
||||
.is_ok()
|
||||
{
|
||||
repaired += 1;
|
||||
} else {
|
||||
requested.remove(&item_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
repaired
|
||||
}
|
||||
|
||||
fn missing_attachments(inner: &AppearanceManagerInner) -> Vec<AttachmentRecord> {
|
||||
let desired = read(&inner.attachments).clone();
|
||||
if desired.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(client) = inner.client.upgrade() else {
|
||||
return desired;
|
||||
};
|
||||
let Some(simulator) = client
|
||||
.native_network()
|
||||
.ok()
|
||||
.and_then(|network| network.native_current_sim())
|
||||
else {
|
||||
return desired;
|
||||
};
|
||||
let avatar_local_id = client
|
||||
.cached_agent_manager_inner()
|
||||
.map_or(0, |agent| agent.avatar_local_id());
|
||||
if avatar_local_id == 0 {
|
||||
return desired;
|
||||
}
|
||||
let present: HashSet<_> = read(&simulator.objects_primitives)
|
||||
.values()
|
||||
.filter(|primitive| primitive.is_attachment && primitive.parent_id == avatar_local_id)
|
||||
.filter_map(|primitive| {
|
||||
crate::appearance::CurrentOutfitFolder::native_get_attachment_item_id(primitive.clone())
|
||||
.ok()
|
||||
})
|
||||
.filter(|item| *item != UUID::zero())
|
||||
.collect();
|
||||
desired
|
||||
.into_iter()
|
||||
.filter(|attachment| !present.contains(&attachment.item.base.uuid()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn complete_baked_appearance(packet: &crate::packets::AvatarAppearancePacket) -> bool {
|
||||
if !matches!(packet.visual_param.len(), 218 | 251) {
|
||||
return false;
|
||||
}
|
||||
let Ok(length) = i32::try_from(packet.object_data.texture_entry.len()) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(textures) = PrimitiveTextureEntry::new_with_bytes_int32_int32(
|
||||
packet.object_data.texture_entry.clone(),
|
||||
0,
|
||||
length,
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
[8_u32, 9, 10, 11].into_iter().all(|index| {
|
||||
textures.get_face(index).ok().flatten().is_some_and(|face| {
|
||||
let id = face.texture_id();
|
||||
id != UUID::zero() && id != default_avatar_texture()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_recovery(
|
||||
inner: &AppearanceManagerInner,
|
||||
phase: AppearanceRecoveryPhase,
|
||||
retry: u32,
|
||||
started: Instant,
|
||||
generation: u64,
|
||||
acknowledged: bool,
|
||||
) {
|
||||
let event = AppearanceRecoveryEvent {
|
||||
phase,
|
||||
stage: AppearanceRecoveryStage::from_u8(inner.recovery_stage.load(Ordering::Acquire)),
|
||||
generation,
|
||||
retry,
|
||||
elapsed_millis: u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
|
||||
cof_version: inner.last_cof_version.load(Ordering::Acquire),
|
||||
wearable_count: read(&inner.wearables).len(),
|
||||
attachment_count: read(&inner.attachments).len(),
|
||||
missing_attachment_count: missing_attachments(inner).len(),
|
||||
cached_bake_count: bake_index_to_texture_index()
|
||||
.into_iter()
|
||||
.filter(|index| {
|
||||
read(&inner.texture_slots)
|
||||
.get(usize::from(*index))
|
||||
.is_some_and(|slot| slot.texture_id != UUID::zero())
|
||||
})
|
||||
.count(),
|
||||
server_baking: inner
|
||||
.client
|
||||
.upgrade()
|
||||
.and_then(|client| client.native_network().ok())
|
||||
.and_then(|network| network.native_current_sim())
|
||||
.is_some_and(|simulator| {
|
||||
simulator.protocols.0 & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0
|
||||
}),
|
||||
appearance_serial: inner.appearance_serial.load(Ordering::Acquire),
|
||||
acknowledged,
|
||||
};
|
||||
inner.events.recovery.emit_with(|| event);
|
||||
}
|
||||
|
||||
impl AppearanceManager {
|
||||
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
||||
let client = client.ok_or(Error::ArgumentNull)?;
|
||||
@@ -395,6 +815,13 @@ impl AppearanceManager {
|
||||
appearance_serial: AtomicU64::new(0),
|
||||
appearance_cancel: Mutex::new(None),
|
||||
appearance_gate: Arc::new(tokio::sync::Semaphore::new(1)),
|
||||
recovery_stage: AtomicU8::new(AppearanceRecoveryStage::Starting as u8),
|
||||
recovery_generation: AtomicU64::new(0),
|
||||
logged_in: AtomicBool::new(false),
|
||||
acknowledgement_sequence: AtomicU64::new(0),
|
||||
acknowledged_cof_version: AtomicI32::new(-1),
|
||||
acknowledged_complete: AtomicBool::new(false),
|
||||
acknowledgement_notify: tokio::sync::Notify::new(),
|
||||
events: AppearanceEvents::default(),
|
||||
login_callback: Mutex::new(None),
|
||||
network_subscriptions: Mutex::new(Vec::new()),
|
||||
@@ -413,23 +840,21 @@ impl AppearanceManager {
|
||||
}
|
||||
},
|
||||
);
|
||||
client
|
||||
.native_network()?
|
||||
.native_register_login_response_callback(
|
||||
login_callback.clone(),
|
||||
Some(vec!["cof_version".to_owned()]),
|
||||
)?;
|
||||
let network = client.native_network()?;
|
||||
network.native_register_login_response_callback(
|
||||
login_callback.clone(),
|
||||
Some(vec!["cof_version".to_owned()]),
|
||||
)?;
|
||||
*mutex(&inner.login_callback) = Some(login_callback);
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let subscription = client
|
||||
.native_network()?
|
||||
.subscribe_raw_packet(Arc::new(move |event| {
|
||||
let Some(inner) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
handle_appearance_packet(&inner, event.packet_type, event.data);
|
||||
}));
|
||||
let subscription = network.subscribe_raw_packet(Arc::new(move |event| {
|
||||
let Some(inner) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
handle_appearance_packet(&inner, event.packet_type, event.data);
|
||||
}));
|
||||
mutex(&inner.network_subscriptions).push(subscription);
|
||||
install_recovery_lifecycle(&inner, &client)?;
|
||||
Ok(Self::native_from_inner(inner))
|
||||
}
|
||||
|
||||
@@ -497,6 +922,14 @@ impl AppearanceManager {
|
||||
self.inner.events.rebake_avatar_requested.subscribe(handler)
|
||||
}
|
||||
|
||||
/// Subscribes to non-secret automatic appearance recovery diagnostics.
|
||||
pub fn subscribe_recovery(
|
||||
&self,
|
||||
handler: EventHandler<AppearanceRecoveryEvent>,
|
||||
) -> Subscription {
|
||||
self.inner.events.recovery.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_texture_provider(&self) -> Arc<dyn crate::IBakingTextureProvider> {
|
||||
Arc::clone(&read(&self.inner.texture_provider))
|
||||
}
|
||||
@@ -510,6 +943,7 @@ impl AppearanceManager {
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible.
|
||||
pub(crate) fn native_dispose(&self) -> Result<(), Error> {
|
||||
cancel_recovery(&self.inner);
|
||||
if let Some(callback) = mutex(&self.inner.login_callback).take()
|
||||
&& let Some(client) = self.inner.client.upgrade()
|
||||
&& let Ok(network) = client.native_network()
|
||||
@@ -1068,7 +1502,19 @@ impl AppearanceManager {
|
||||
} else {
|
||||
link.base.uuid()
|
||||
};
|
||||
let Some(item) = store.native_item_value(target_id) else {
|
||||
let item = if let Some(item) = store.native_item_value(target_id) {
|
||||
item
|
||||
} else if let Some(item) = inventory
|
||||
.native_fetch_item(
|
||||
target_id,
|
||||
link.base.owner_id(),
|
||||
cancellation_token.clone(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
item
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
worn.push(item.base.clone());
|
||||
@@ -1393,6 +1839,7 @@ impl AppearanceManager {
|
||||
&self,
|
||||
force_rebake: bool,
|
||||
) -> Result<(), Error> {
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Starting);
|
||||
let source = CancellationTokenSource::new();
|
||||
if let Some(previous) = mutex(&self.inner.appearance_cancel).replace(source.clone()) {
|
||||
previous.cancel();
|
||||
@@ -1419,6 +1866,7 @@ impl AppearanceManager {
|
||||
.native_current_sim()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let use_server_baking = self.native_server_baking_available()?;
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Outfit);
|
||||
self.native_refresh_worn_for_bake(use_server_baking, token.clone())
|
||||
.await?;
|
||||
if force_rebake {
|
||||
@@ -1431,9 +1879,11 @@ impl AppearanceManager {
|
||||
}
|
||||
}
|
||||
if use_server_baking {
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Bake);
|
||||
self.native_server_bake(token.clone()).await?;
|
||||
} else {
|
||||
if !force_rebake && !read(&self.inner.wearables).is_empty() {
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Cache);
|
||||
let generation = self.inner.cache_reply_generation.load(Ordering::Acquire);
|
||||
let notified = self.inner.cache_notify.notified();
|
||||
let request_sent = self.native_request_cached_bakes_sent()?;
|
||||
@@ -1449,11 +1899,14 @@ impl AppearanceManager {
|
||||
let _cache_reply_received =
|
||||
self.inner.cache_reply_generation.load(Ordering::Acquire) != generation;
|
||||
}
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Assets);
|
||||
self.native_gather_baking_inputs(token.clone()).await?;
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Bake);
|
||||
self.native_local_bake(Some(token.clone()), force_rebake)
|
||||
.await?;
|
||||
}
|
||||
token.throw_if_cancellation_requested()?;
|
||||
set_recovery_stage(&self.inner, AppearanceRecoveryStage::Publish);
|
||||
let packet = self.native_make_appearance_packet()?;
|
||||
let network = self.client()?.native_network()?;
|
||||
if network.native_current_sim().as_ref() != Some(&start_simulator) {
|
||||
@@ -2706,6 +3159,58 @@ mod tests {
|
||||
assert_eq!(manager.native_last_cof_version(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoritative_appearance_requires_bakes_visuals_and_current_cof() {
|
||||
let client = GridClient::new().unwrap();
|
||||
let manager = client.appearance();
|
||||
manager.native_update_cof_version(12).unwrap();
|
||||
let mut packet = crate::packets::AvatarAppearancePacket::new_with_constructor().unwrap();
|
||||
packet.sender.id = client.network().native_agent_id();
|
||||
let mut appearance =
|
||||
crate::packets::AvatarAppearancePacketAppearanceDataBlock::new_with_constructor()
|
||||
.unwrap();
|
||||
appearance.cof_version = 12;
|
||||
packet.appearance_data.push(appearance);
|
||||
packet.visual_param = (0..218)
|
||||
.map(|_| {
|
||||
crate::packets::AvatarAppearancePacketVisualParamBlock::new_with_constructor()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut texture_ids = vec![UUID::zero(); 12];
|
||||
for (index, value) in [8_usize, 9, 10, 11].into_iter().enumerate() {
|
||||
texture_ids[value] = UUID::new_with_u_int64(10_000 + index as u64).unwrap();
|
||||
}
|
||||
packet.object_data.texture_entry = encode_texture_entry(&texture_ids).unwrap();
|
||||
|
||||
assert!(complete_baked_appearance(&packet));
|
||||
handle_appearance_packet(
|
||||
&manager.inner,
|
||||
PacketType::AvatarAppearance,
|
||||
packet.to_bytes_with_method().unwrap(),
|
||||
);
|
||||
assert!(manager.inner.acknowledged_complete.load(Ordering::Acquire));
|
||||
assert_eq!(
|
||||
manager
|
||||
.inner
|
||||
.acknowledged_cof_version
|
||||
.load(Ordering::Acquire),
|
||||
12
|
||||
);
|
||||
|
||||
packet.visual_param.clear();
|
||||
assert!(!complete_baked_appearance(&packet));
|
||||
packet.visual_param = (0..218)
|
||||
.map(|_| {
|
||||
crate::packets::AvatarAppearancePacketVisualParamBlock::new_with_constructor()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
texture_ids[8] = default_avatar_texture();
|
||||
packet.object_data.texture_entry = encode_texture_entry(&texture_ids).unwrap();
|
||||
assert!(!complete_baked_appearance(&packet));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_appearance_request_emits_failure_and_empty_cache_request_does_not_wait() {
|
||||
let client = GridClient::new().unwrap();
|
||||
|
||||
@@ -24,11 +24,11 @@ use crate::{
|
||||
};
|
||||
use futures_channel::oneshot;
|
||||
use futures_util::{FutureExt, pin_mut, select_biased};
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, CancellationTokenSource, EventHandler, Subscription, Uri,
|
||||
};
|
||||
use libremetaverse_types::{AssetType, UUID, Utils};
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -54,6 +54,13 @@ fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_response(bytes: Vec<u8>) -> Result<HashMap<String, OSD>, Error> {
|
||||
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
@@ -1147,32 +1154,36 @@ impl AssetManager {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let meta: Value = serde_json::from_slice(&metadata).map_err(|_| Error::Argument)?;
|
||||
let meta = upload_response(metadata)?;
|
||||
let uploader = meta
|
||||
.get("uploader")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or(Error::Argument)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::Argument)?;
|
||||
let (_, response) = client
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
Uri(uploader.to_owned()),
|
||||
uploader,
|
||||
"application/octet-stream".into(),
|
||||
data,
|
||||
token,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let result: Value = serde_json::from_slice(&response).map_err(|_| Error::Argument)?;
|
||||
if result.get("state").and_then(Value::as_str) != Some("complete") {
|
||||
let result = upload_response(response)?;
|
||||
if result
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.as_deref()
|
||||
!= Some("complete")
|
||||
{
|
||||
return Ok(UUID::zero());
|
||||
}
|
||||
result
|
||||
.get("new_asset")
|
||||
.or_else(|| result.get("new_asset_id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|id| UUID::new_with_string(id.to_owned()))
|
||||
.transpose()
|
||||
.map(|id| id.unwrap_or_else(UUID::zero))
|
||||
.map_or(Ok(UUID::zero()), OSD::as_uuid)
|
||||
}
|
||||
pub async fn request_upload_baked_texture(
|
||||
&self,
|
||||
@@ -1411,3 +1422,29 @@ impl AssetManager {
|
||||
self.inner.events.xfer.subscribe(handler)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn baked_upload_responses_are_llsd_maps() {
|
||||
let id = "12345678-1234-1234-1234-123456789abc";
|
||||
let metadata = upload_response(
|
||||
b"<llsd><map><key>uploader</key><uri>https://grid.invalid/upload</uri></map></llsd>"
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
metadata["uploader"].as_uri().unwrap().unwrap().0,
|
||||
"https://grid.invalid/upload"
|
||||
);
|
||||
let complete = upload_response(
|
||||
format!("<llsd><map><key>state</key><string>complete</string><key>new_asset</key><uuid>{id}</uuid></map></llsd>")
|
||||
.into_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(complete["state"].as_string().unwrap(), "complete");
|
||||
assert_eq!(complete["new_asset"].as_uuid().unwrap().to_string(), id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ mod animesh_runtime;
|
||||
mod animesh_skinning;
|
||||
mod appearance_baker;
|
||||
mod appearance_manager;
|
||||
pub use appearance_manager::{
|
||||
AppearanceRecoveryEvent, AppearanceRecoveryPhase, AppearanceRecoveryStage,
|
||||
};
|
||||
mod asset_archive;
|
||||
mod asset_cache;
|
||||
mod asset_manager;
|
||||
|
||||
@@ -3697,6 +3697,14 @@ impl NetworkManager {
|
||||
self.update_login_status(LoginStatus::Failed, connect_failure_message.to_owned());
|
||||
return Ok(false);
|
||||
}
|
||||
if self.inner.client.settings_ref().agent.send_appearance {
|
||||
self.inner
|
||||
.client
|
||||
.native_appearance()?
|
||||
.native_update_cof_version(
|
||||
i32::try_from(response.cof_version()).unwrap_or(i32::MAX),
|
||||
)?;
|
||||
}
|
||||
self.update_login_status(LoginStatus::Success, success_message);
|
||||
Ok(true)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user