diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/Components/IAvatarAttachment.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/Components/IAvatarAttachment.cs index f2e956d96f0..26f45caa525 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/Components/IAvatarAttachment.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/Components/IAvatarAttachment.cs @@ -149,6 +149,8 @@ public bool TryGetContentHashByKey(string key, out string? hash) return true; } } + else if (string.IsNullOrEmpty(key)) + ReportHub.LogWarning(ReportCategory.WEARABLE, $"Empty content key requested for wearable with ID: {DTO.Metadata.id}"); else ReportHub.LogError(ReportCategory.WEARABLE, $"No content found in DTO for wearable with ID: {DTO.Metadata.id}"); diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs index b868526a369..d191547eb3d 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs @@ -42,8 +42,7 @@ public static Representation NewFakeRepresentation() => [Serializable] public abstract class MetadataBase : TrimmedAvatarAttachmentDTO.TrimmedMetadataBase { - public string name; - + // `name` is inherited from TrimmedMetadataBase. public I18n[] i18n; public string thumbnail; diff --git a/Explorer/Assets/DCL/CommunicationData/URLHelpers/URN.cs b/Explorer/Assets/DCL/CommunicationData/URLHelpers/URN.cs index 00c1a085714..135ab2347f7 100644 --- a/Explorer/Assets/DCL/CommunicationData/URLHelpers/URN.cs +++ b/Explorer/Assets/DCL/CommunicationData/URLHelpers/URN.cs @@ -17,9 +17,12 @@ public readonly struct URN public URN(string urn) { + // A null/empty urn produces the same field state as default(URN), which IsNullOrEmpty and + // IsValid already treat as a first-class value; ToLower() on it crashed emote loading when + // a DTO carried no id. originalUrn = urn; - lowercaseUrn = originalUrn.ToLower(); - hash = originalUrn != null ? lowercaseUrn.GetHashCode() : 0; + lowercaseUrn = string.IsNullOrEmpty(urn) ? urn : urn.ToLower(); + hash = string.IsNullOrEmpty(urn) ? 0 : lowercaseUrn.GetHashCode(); } private URN(URN urn, int shortenIndex) diff --git a/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.CompactInfo.cs b/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.CompactInfo.cs index 2bed663c5dd..3e172633346 100644 --- a/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.CompactInfo.cs +++ b/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.CompactInfo.cs @@ -284,7 +284,9 @@ public void Dispose() PicturePromise?.LoadingIntention.CancellationTokenSource.Cancel(); PicturePromise = null; + // Nulling the reference keeps Dispose idempotent for any caller that reaches it twice. ProfilePicture.TryDereference(); + ProfilePicture = null; } } } diff --git a/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.cs b/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.cs index 458611ca638..5a521d06c3b 100644 --- a/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.cs +++ b/Explorer/Assets/DCL/Profiles/SharedAPI/Profile.cs @@ -83,7 +83,8 @@ public Profile(string userId, string name, Avatar avatar) public void Dispose() { - GetCompact().Dispose(); + // POOL's actionOnRelease runs Clear(), which already disposes the compact info - + // disposing it here too made every pooled release a double-dispose. POOL.Release(this); } diff --git a/Explorer/Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs b/Explorer/Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs index 946a0a1fb6f..b21ccfbe836 100644 --- a/Explorer/Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs +++ b/Explorer/Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs @@ -57,6 +57,7 @@ public partial class SmartWearableSystem : BaseUnityLoopSystem /// private readonly Dictionary pendingScenes = new (); + private readonly CancellationTokenSource systemCts = new (); private CancellationTokenSource outfitEquipCts = new (); private bool currentSceneDirty; @@ -95,6 +96,21 @@ public override void Initialize() web3IdentityCache.OnIdentityCleared += OnIdentityCleared; } + protected override void OnDispose() + { + backpackEventBus.EquipWearableEvent -= OnEquipWearable; + backpackEventBus.UnEquipWearableEvent -= OnUnEquipWearable; + backpackEventBus.EquipOutfitEvent -= OnEquipOutfit; + portableExperiencesController.PortableExperienceUnloaded -= OnPortableExperienceUnloaded; + loadingStatus.CurrentStage.OnUpdate -= OnLoadingStatusChanged; + scenesCache.CurrentScene.OnUpdate -= OnCurrentSceneChanged; + web3IdentityCache.OnIdentityCleared -= OnIdentityCleared; + + systemCts.Cancel(); + systemCts.Dispose(); + outfitEquipCts.SafeCancelAndDispose(); + } + private void OnEquipWearable(IWearable wearable, bool isManuallyEquipped) { if (!isManuallyEquipped) return; @@ -104,7 +120,7 @@ private void OnEquipWearable(IWearable wearable, bool isManuallyEquipped) private async UniTask TryRunSmartWearableSceneAsync(IWearable wearable) { - bool isSmart = await smartWearableCache.IsSmartAsync(wearable, CancellationToken.None); + bool isSmart = await smartWearableCache.IsSmartAsync(wearable, systemCts.Token); if (!isSmart || !smartWearableCache.CurrentSceneAllowsSmartWearables) return; string id = SmartWearableCache.GetCacheId(wearable); @@ -322,7 +338,7 @@ private void HandleSceneChange() if (smartWearablesAllowed) // Notice scenes that are already running won't run again, so we can call this safely // TODO consider cancelling a previous running task - RunScenesForEquippedWearablesAsync(AuthorizationAction.SkipAuthorization, CancellationToken.None).Forget(); + RunScenesForEquippedWearablesAsync(AuthorizationAction.SkipAuthorization, systemCts.Token).Forget(); else UnloadAllSmartWearableScenes(); } @@ -348,7 +364,7 @@ private void OnLoadingStatusChanged(LoadingStatus.LoadingStage stage) loadingStatus.CurrentStage.OnUpdate -= OnLoadingStatusChanged; scenesCache.CurrentScene.OnUpdate += OnCurrentSceneChanged; - RunScenesForEquippedWearablesAsync(AuthorizationAction.RequestAuthorization, CancellationToken.None).Forget(); + RunScenesForEquippedWearablesAsync(AuthorizationAction.RequestAuthorization, systemCts.Token).Forget(); } private async UniTask RunScenesForEquippedWearablesAsync(AuthorizationAction authorization, CancellationToken ct)