From 6388921e4c9d0f6def74980a8ace1ca667ebb8a3 Mon Sep 17 00:00:00 2001 From: Pravus Date: Mon, 9 Jun 2025 20:30:34 +0200 Subject: [PATCH 1/2] refactored to have the builder emotes promise creation in a dedicated system so that several collections are supported --- .../Systems/Load/LoadOwnedEmotesSystem.cs | 99 +----------- .../ResolveBuilderEmotePromisesSystem.cs | 150 ++++++++++++++++++ .../ResolveBuilderEmotePromisesSystem.cs.meta | 2 + .../DCL/PluginSystem/Global/EmotePlugin.cs | 7 +- 4 files changed, 157 insertions(+), 101 deletions(-) create mode 100644 Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs create mode 100644 Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs.meta diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs index c48c0f79ad1..21e71b6d335 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs @@ -4,17 +4,12 @@ using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Loading; -using DCL.AvatarRendering.Loading.Components; using DCL.AvatarRendering.Loading.Systems.Abstract; using DCL.Diagnostics; -using DCL.SDKComponents.AudioSources; using DCL.WebRequests; using ECS; -using ECS.Prioritization.Components; using ECS.StreamableLoading.Cache; -using ECS.StreamableLoading.GLTF; using System; -using GltfPromise = ECS.StreamableLoading.Common.AssetPromise; namespace DCL.AvatarRendering.Emotes.Load { @@ -22,10 +17,7 @@ namespace DCL.AvatarRendering.Emotes.Load [LogCategory(ReportCategory.EMOTE)] public partial class LoadOwnedEmotesSystem : LoadElementsByIntentionSystem { - private static readonly BodyShape[] ALL_BODYSHAPES = { BodyShape.MALE, BodyShape.FEMALE }; - internal IURLBuilder urlBuilder = new URLBuilder(); - private readonly IEmoteStorage emoteStorage; public LoadOwnedEmotesSystem( World world, @@ -36,7 +28,6 @@ public LoadOwnedEmotesSystem( string? builderContentURL = null ) : base(world, cache, emoteStorage, webRequestController, realmData, builderContentURL, "emote") { - this.emoteStorage = emoteStorage; } protected override async UniTask>> ParseResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) => @@ -61,96 +52,8 @@ protected override URLAddress BuildUrlFromIntention(in GetOwnedEmotesFromRealmIn protected override EmotesResolution AssetFromPreparedIntention(in GetOwnedEmotesFromRealmIntention intention) { - // Create asset promises for builder collection emotes after DTO loading is complete - if (intention.NeedsBuilderAPISigning) - { - foreach (IEmote emote in intention.Result.List) - { - TryCreateBuilderEmoteAssetPromises(emote); - } - } - + // Promise creation for builder collections is handled at ResolveBuilderEmotePromisesSystem return new EmotesResolution(intention.Result, intention.TotalAmount); } - - private bool TryCreateBuilderEmoteAssetPromises(IEmote emote) - { - if (string.IsNullOrEmpty(emote.DTO.ContentDownloadUrl)) - return false; - - // Check if emote already has assets loaded or is loading - if (emote.IsLoading) - return false; - - // Check if we already have this emote in storage with assets - if (emoteStorage.TryGetElement(emote.GetUrn(), out IEmote existingEmote)) - { - // For unisex emotes with same clip, check if either bodyshape has assets - if (existingEmote.IsUnisex() && existingEmote.HasSameClipForAllGenders()) - { - if (existingEmote.AssetResults[BodyShape.MALE] != null || existingEmote.AssetResults[BodyShape.FEMALE] != null) - return false; - } - else - { - // For non-unisex emotes, check both bodyshapes - if (existingEmote.AssetResults[BodyShape.MALE] != null && existingEmote.AssetResults[BodyShape.FEMALE] != null) - return false; - } - } - - bool foundGlb = false; - - BodyShape? targetBodyShape = null; - if (!emote.IsUnisex()) - targetBodyShape = BodyShape.FromStringSafe(emote.DTO.Metadata.AbstractData.representations[0].bodyShapes[0]); - - // The resolution of these promises will be finalized by FinalizeEmoteLoadingSystem - foreach (var content in emote.DTO.content) - { - if (content.file.EndsWith(".glb")) - { - for (int i = 0; i < ALL_BODYSHAPES.Length; i++) - { - BodyShape bodyShape = ALL_BODYSHAPES[i]; - if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape!)) - continue; - - // Skip if this bodyshape already has an asset result - if (emote.AssetResults[bodyShape] != null) - continue; - - var gltfPromise = GltfPromise.Create(World, GetGLTFIntention.Create(content.file, content.hash), PartitionComponent.TOP_PRIORITY); - World.Create(gltfPromise, emote, bodyShape); - emote.UpdateLoadingStatus(true); - foundGlb = true; - } - continue; - } - - // Supported audio format in emotes: https://docs.decentraland.org/creator/emotes/props-and-sounds/#add-audio-to-the-emotes - if (content.file.EndsWith(".mp3") || content.file.EndsWith(".ogg")) - { - var audioType = content.file.ToAudioType(); - urlBuilder.Clear(); - urlBuilder.AppendDomain(URLDomain.FromString(emote.DTO.ContentDownloadUrl)).AppendPath(new URLPath(content.hash)); - URLAddress url = urlBuilder.Build(); - - for (int i = 0; i < ALL_BODYSHAPES.Length; i++) - { - BodyShape bodyShape = ALL_BODYSHAPES[i]; - if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape)) - continue; - - var audioPromise = AudioUtils.CreateAudioClipPromise(World, url.Value, audioType, PartitionComponent.TOP_PRIORITY); - World.Create(audioPromise, emote, bodyShape); - } - - if (foundGlb) break; - } - } - - return foundGlb; - } } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs new file mode 100644 index 00000000000..91006e74933 --- /dev/null +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs @@ -0,0 +1,150 @@ +using Arch.Core; +using Arch.System; +using Arch.SystemGroups; +using Arch.SystemGroups.DefaultSystemGroups; +using CommunicationData.URLHelpers; +using DCL.AvatarRendering.Loading.Components; +using DCL.Diagnostics; +using DCL.SDKComponents.AudioSources; +using ECS.Abstract; +using ECS.Prioritization.Components; +using ECS.StreamableLoading.GLTF; +using System; + +using StreamableResult = ECS.StreamableLoading.Common.Components.StreamableLoadingResult; +using GltfPromise = ECS.StreamableLoading.Common.AssetPromise; + +namespace DCL.AvatarRendering.Emotes.Systems +{ + [UpdateInGroup(typeof(PresentationSystemGroup))] + [UpdateAfter(typeof(FinalizeEmoteLoadingSystem))] + [LogCategory(ReportCategory.EMOTE)] + public partial class ResolveBuilderEmotePromisesSystem : BaseUnityLoopSystem + { + private static readonly BodyShape[] ALL_BODYSHAPES = { BodyShape.MALE, BodyShape.FEMALE }; + + private readonly IEmoteStorage emoteStorage; + internal readonly IURLBuilder urlBuilder = new URLBuilder(); + + public ResolveBuilderEmotePromisesSystem( + World world, + IEmoteStorage emoteStorage + ) : base(world) + { + this.emoteStorage = emoteStorage; + } + + protected override void Update(float t) + { + ResolveBuilderEmotePromiseQuery(World); + } + + [Query] + [None(typeof(StreamableResult))] + private void ResolveBuilderEmotePromise(Entity entity, ref GetOwnedEmotesFromRealmIntention intention, ref IPartitionComponent partitionComponent) + { + if (intention.CancellationTokenSource.IsCancellationRequested) + { + World!.Add(entity, new StreamableResult(GetReportCategory(), new Exception("Emotes request cancelled"))); + return; + } + + // Only create promises for builder collections + if (intention is { NeedsBuilderAPISigning: false } || intention.Result.List is not { Count: > 0 }) + return; + + bool allEmotesProcessed = true; + + foreach (IEmote emote in intention.Result.List) + { + if (TryCreateBuilderEmoteAssetPromises(emote, partitionComponent)) + allEmotesProcessed = false; + } + + if (allEmotesProcessed) + World!.Add(entity, new StreamableResult(new EmotesResolution(intention.Result, intention.TotalAmount))); + } + + private bool TryCreateBuilderEmoteAssetPromises(in IEmote emote, in IPartitionComponent partitionComponent) + { + if (string.IsNullOrEmpty(emote.DTO.ContentDownloadUrl)) + return false; + + // Check if emote already has assets loaded or is loading + if (emote.IsLoading) + return true; // Still processing, not done yet + + // Check if we already have this emote in storage with assets + if (emoteStorage.TryGetElement(emote.GetUrn(), out IEmote existingEmote)) + { + // For unisex emotes with same clip, check if either bodyshape has assets + if (existingEmote.IsUnisex() && existingEmote.HasSameClipForAllGenders()) + { + if (existingEmote.AssetResults[BodyShape.MALE] != null || existingEmote.AssetResults[BodyShape.FEMALE] != null) + return false; // Already processed + } + else + { + // For non-unisex emotes, check both bodyshapes + if (existingEmote.AssetResults[BodyShape.MALE] != null && existingEmote.AssetResults[BodyShape.FEMALE] != null) + return false; // Already processed + } + } + + bool foundGlb = false; + bool stillProcessing = false; + + BodyShape? targetBodyShape = null; + if (!emote.IsUnisex()) + targetBodyShape = BodyShape.FromStringSafe(emote.DTO.Metadata.AbstractData.representations[0].bodyShapes[0]); + + // The resolution of these promises will be finalized by FinalizeEmoteLoadingSystem + foreach (var content in emote.DTO.content) + { + if (content.file.EndsWith(".glb")) + { + for (int i = 0; i < ALL_BODYSHAPES.Length; i++) + { + BodyShape bodyShape = ALL_BODYSHAPES[i]; + if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape!)) + continue; + + // Skip if this bodyshape already has an asset result + if (emote.AssetResults[bodyShape] != null) + continue; + + var gltfPromise = GltfPromise.Create(World!, GetGLTFIntention.Create(content.file, content.hash), partitionComponent); + World!.Create(gltfPromise, emote, bodyShape); + emote.UpdateLoadingStatus(true); + foundGlb = true; + stillProcessing = true; + } + continue; + } + + // Supported audio format in emotes: https://docs.decentraland.org/creator/emotes/props-and-sounds/#add-audio-to-the-emotes + if (content.file.EndsWith(".mp3") || content.file.EndsWith(".ogg")) + { + var audioType = content.file.ToAudioType(); + urlBuilder.Clear(); + urlBuilder.AppendDomain(URLDomain.FromString(emote.DTO.ContentDownloadUrl)).AppendPath(new URLPath(content.hash)); + URLAddress url = urlBuilder.Build(); + + for (int i = 0; i < ALL_BODYSHAPES.Length; i++) + { + BodyShape bodyShape = ALL_BODYSHAPES[i]; + if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape)) + continue; + + var audioPromise = AudioUtils.CreateAudioClipPromise(World!, url.Value, audioType, partitionComponent); + World!.Create(audioPromise, emote, bodyShape); + } + + if (foundGlb) break; + } + } + + return stillProcessing; + } + } +} diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs.meta new file mode 100644 index 00000000000..eb77c72d229 --- /dev/null +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a048e45735f404d79bf7c088846f36a4 \ No newline at end of file diff --git a/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs index 8ed2db2d879..77be7b63bc2 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs @@ -4,6 +4,7 @@ using Cysharp.Threading.Tasks; using DCL.AssetsProvision; using DCL.AvatarRendering.Emotes; +using DCL.AvatarRendering.Emotes.Systems; using DCL.AvatarRendering.Loading.Components; using DCL.AvatarRendering.Wearables; using DCL.Backpack; @@ -15,14 +16,11 @@ using DCL.Profiles.Self; using DCL.Web3.Identities; using DCL.ResourcesUnloading; -using DCL.UI.MainUI; using DCL.UI.SharedSpaceManager; using DCL.WebRequests; using ECS; using ECS.StreamableLoading.AudioClips; using ECS.StreamableLoading.Cache; -using ECS.StreamableLoading.GLTF; -using ECS.StreamableLoading.GLTF.DownloadProvider; using Global.AppArgs; using MVC; using System; @@ -133,6 +131,9 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, new NoCache(false, false), emoteStorage, builderContentURL); + if(builderCollectionsPreview) + ResolveBuilderEmotePromisesSystem.InjectToWorld(ref builder, emoteStorage); + CharacterEmoteSystem.InjectToWorld(ref builder, emoteStorage, messageBus, audioSourceReference, debugBuilder, localSceneDevelopment, appArgs); LoadAudioClipGlobalSystem.InjectToWorld(ref builder, audioClipsCache, webRequestController); From 0f1601c898dc3944f8e1a3d855d165aeef029be2 Mon Sep 17 00:00:00 2001 From: Pravus Date: Tue, 10 Jun 2025 15:59:14 +0200 Subject: [PATCH 2/2] replaced base Exceptions by more correct OperationCanceledException --- .../Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs | 2 +- .../Wearables/Systems/ResolveWearablePromisesSystem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs index 91006e74933..ea88d55bd9b 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs @@ -45,7 +45,7 @@ private void ResolveBuilderEmotePromise(Entity entity, ref GetOwnedEmotesFromRea { if (intention.CancellationTokenSource.IsCancellationRequested) { - World!.Add(entity, new StreamableResult(GetReportCategory(), new Exception("Emotes request cancelled"))); + World!.Add(entity, new StreamableResult(GetReportCategory(), new OperationCanceledException("Emotes request cancelled"))); return; } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearablePromisesSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearablePromisesSystem.cs index 4a418513a4c..514da83e208 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearablePromisesSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearablePromisesSystem.cs @@ -62,7 +62,7 @@ private void ResolveWearablePromise([Data] bool defaultWearablesResolved, in Ent { if (wearablesByPointersIntention.CancellationTokenSource.IsCancellationRequested) { - World!.Add(entity, new StreamableResult(GetReportCategory(), new Exception("Pointer request cancelled"))); + World!.Add(entity, new StreamableResult(GetReportCategory(), new OperationCanceledException("Pointer request cancelled"))); return; }