Skip to content

Commit 6288631

Browse files
authored
fix: support multiple unreleased builder emote collections preview (#4379)
Refactored to have the builder emotes promise creation in a dedicated system so that several collections are supported
1 parent 39e40d4 commit 6288631

5 files changed

Lines changed: 158 additions & 102 deletions

File tree

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs

Lines changed: 1 addition & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,20 @@
44
using CommunicationData.URLHelpers;
55
using Cysharp.Threading.Tasks;
66
using DCL.AvatarRendering.Loading;
7-
using DCL.AvatarRendering.Loading.Components;
87
using DCL.AvatarRendering.Loading.Systems.Abstract;
98
using DCL.Diagnostics;
10-
using DCL.SDKComponents.AudioSources;
119
using DCL.WebRequests;
1210
using ECS;
13-
using ECS.Prioritization.Components;
1411
using ECS.StreamableLoading.Cache;
15-
using ECS.StreamableLoading.GLTF;
1612
using System;
17-
using GltfPromise = ECS.StreamableLoading.Common.AssetPromise<ECS.StreamableLoading.GLTF.GLTFData, ECS.StreamableLoading.GLTF.GetGLTFIntention>;
1813

1914
namespace DCL.AvatarRendering.Emotes.Load
2015
{
2116
[UpdateInGroup(typeof(PresentationSystemGroup))]
2217
[LogCategory(ReportCategory.EMOTE)]
2318
public partial class LoadOwnedEmotesSystem : LoadElementsByIntentionSystem<EmotesResolution, GetOwnedEmotesFromRealmIntention, IEmote, EmoteDTO>
2419
{
25-
private static readonly BodyShape[] ALL_BODYSHAPES = { BodyShape.MALE, BodyShape.FEMALE };
26-
2720
internal IURLBuilder urlBuilder = new URLBuilder();
28-
private readonly IEmoteStorage emoteStorage;
2921

3022
public LoadOwnedEmotesSystem(
3123
World world,
@@ -36,7 +28,6 @@ public LoadOwnedEmotesSystem(
3628
string? builderContentURL = null
3729
) : base(world, cache, emoteStorage, webRequestController, realmData, builderContentURL, "emote")
3830
{
39-
this.emoteStorage = emoteStorage;
4031
}
4132

4233
protected override async UniTask<IAttachmentLambdaResponse<ILambdaResponseElement<EmoteDTO>>> ParseResponseAsync(GenericDownloadHandlerUtils.Adapter<GenericGetRequest, GenericGetArguments> adapter) =>
@@ -61,96 +52,8 @@ protected override URLAddress BuildUrlFromIntention(in GetOwnedEmotesFromRealmIn
6152

6253
protected override EmotesResolution AssetFromPreparedIntention(in GetOwnedEmotesFromRealmIntention intention)
6354
{
64-
// Create asset promises for builder collection emotes after DTO loading is complete
65-
if (intention.NeedsBuilderAPISigning)
66-
{
67-
foreach (IEmote emote in intention.Result.List)
68-
{
69-
TryCreateBuilderEmoteAssetPromises(emote);
70-
}
71-
}
72-
55+
// Promise creation for builder collections is handled at ResolveBuilderEmotePromisesSystem
7356
return new EmotesResolution(intention.Result, intention.TotalAmount);
7457
}
75-
76-
private bool TryCreateBuilderEmoteAssetPromises(IEmote emote)
77-
{
78-
if (string.IsNullOrEmpty(emote.DTO.ContentDownloadUrl))
79-
return false;
80-
81-
// Check if emote already has assets loaded or is loading
82-
if (emote.IsLoading)
83-
return false;
84-
85-
// Check if we already have this emote in storage with assets
86-
if (emoteStorage.TryGetElement(emote.GetUrn(), out IEmote existingEmote))
87-
{
88-
// For unisex emotes with same clip, check if either bodyshape has assets
89-
if (existingEmote.IsUnisex() && existingEmote.HasSameClipForAllGenders())
90-
{
91-
if (existingEmote.AssetResults[BodyShape.MALE] != null || existingEmote.AssetResults[BodyShape.FEMALE] != null)
92-
return false;
93-
}
94-
else
95-
{
96-
// For non-unisex emotes, check both bodyshapes
97-
if (existingEmote.AssetResults[BodyShape.MALE] != null && existingEmote.AssetResults[BodyShape.FEMALE] != null)
98-
return false;
99-
}
100-
}
101-
102-
bool foundGlb = false;
103-
104-
BodyShape? targetBodyShape = null;
105-
if (!emote.IsUnisex())
106-
targetBodyShape = BodyShape.FromStringSafe(emote.DTO.Metadata.AbstractData.representations[0].bodyShapes[0]);
107-
108-
// The resolution of these promises will be finalized by FinalizeEmoteLoadingSystem
109-
foreach (var content in emote.DTO.content)
110-
{
111-
if (content.file.EndsWith(".glb"))
112-
{
113-
for (int i = 0; i < ALL_BODYSHAPES.Length; i++)
114-
{
115-
BodyShape bodyShape = ALL_BODYSHAPES[i];
116-
if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape!))
117-
continue;
118-
119-
// Skip if this bodyshape already has an asset result
120-
if (emote.AssetResults[bodyShape] != null)
121-
continue;
122-
123-
var gltfPromise = GltfPromise.Create(World, GetGLTFIntention.Create(content.file, content.hash), PartitionComponent.TOP_PRIORITY);
124-
World.Create(gltfPromise, emote, bodyShape);
125-
emote.UpdateLoadingStatus(true);
126-
foundGlb = true;
127-
}
128-
continue;
129-
}
130-
131-
// Supported audio format in emotes: https://docs.decentraland.org/creator/emotes/props-and-sounds/#add-audio-to-the-emotes
132-
if (content.file.EndsWith(".mp3") || content.file.EndsWith(".ogg"))
133-
{
134-
var audioType = content.file.ToAudioType();
135-
urlBuilder.Clear();
136-
urlBuilder.AppendDomain(URLDomain.FromString(emote.DTO.ContentDownloadUrl)).AppendPath(new URLPath(content.hash));
137-
URLAddress url = urlBuilder.Build();
138-
139-
for (int i = 0; i < ALL_BODYSHAPES.Length; i++)
140-
{
141-
BodyShape bodyShape = ALL_BODYSHAPES[i];
142-
if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape))
143-
continue;
144-
145-
var audioPromise = AudioUtils.CreateAudioClipPromise(World, url.Value, audioType, PartitionComponent.TOP_PRIORITY);
146-
World.Create(audioPromise, emote, bodyShape);
147-
}
148-
149-
if (foundGlb) break;
150-
}
151-
}
152-
153-
return foundGlb;
154-
}
15558
}
15659
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
using Arch.Core;
2+
using Arch.System;
3+
using Arch.SystemGroups;
4+
using Arch.SystemGroups.DefaultSystemGroups;
5+
using CommunicationData.URLHelpers;
6+
using DCL.AvatarRendering.Loading.Components;
7+
using DCL.Diagnostics;
8+
using DCL.SDKComponents.AudioSources;
9+
using ECS.Abstract;
10+
using ECS.Prioritization.Components;
11+
using ECS.StreamableLoading.GLTF;
12+
using System;
13+
14+
using StreamableResult = ECS.StreamableLoading.Common.Components.StreamableLoadingResult<DCL.AvatarRendering.Emotes.EmotesResolution>;
15+
using GltfPromise = ECS.StreamableLoading.Common.AssetPromise<ECS.StreamableLoading.GLTF.GLTFData, ECS.StreamableLoading.GLTF.GetGLTFIntention>;
16+
17+
namespace DCL.AvatarRendering.Emotes.Systems
18+
{
19+
[UpdateInGroup(typeof(PresentationSystemGroup))]
20+
[UpdateAfter(typeof(FinalizeEmoteLoadingSystem))]
21+
[LogCategory(ReportCategory.EMOTE)]
22+
public partial class ResolveBuilderEmotePromisesSystem : BaseUnityLoopSystem
23+
{
24+
private static readonly BodyShape[] ALL_BODYSHAPES = { BodyShape.MALE, BodyShape.FEMALE };
25+
26+
private readonly IEmoteStorage emoteStorage;
27+
internal readonly IURLBuilder urlBuilder = new URLBuilder();
28+
29+
public ResolveBuilderEmotePromisesSystem(
30+
World world,
31+
IEmoteStorage emoteStorage
32+
) : base(world)
33+
{
34+
this.emoteStorage = emoteStorage;
35+
}
36+
37+
protected override void Update(float t)
38+
{
39+
ResolveBuilderEmotePromiseQuery(World);
40+
}
41+
42+
[Query]
43+
[None(typeof(StreamableResult))]
44+
private void ResolveBuilderEmotePromise(Entity entity, ref GetOwnedEmotesFromRealmIntention intention, ref IPartitionComponent partitionComponent)
45+
{
46+
if (intention.CancellationTokenSource.IsCancellationRequested)
47+
{
48+
World!.Add(entity, new StreamableResult(GetReportCategory(), new OperationCanceledException("Emotes request cancelled")));
49+
return;
50+
}
51+
52+
// Only create promises for builder collections
53+
if (intention is { NeedsBuilderAPISigning: false } || intention.Result.List is not { Count: > 0 })
54+
return;
55+
56+
bool allEmotesProcessed = true;
57+
58+
foreach (IEmote emote in intention.Result.List)
59+
{
60+
if (TryCreateBuilderEmoteAssetPromises(emote, partitionComponent))
61+
allEmotesProcessed = false;
62+
}
63+
64+
if (allEmotesProcessed)
65+
World!.Add(entity, new StreamableResult(new EmotesResolution(intention.Result, intention.TotalAmount)));
66+
}
67+
68+
private bool TryCreateBuilderEmoteAssetPromises(in IEmote emote, in IPartitionComponent partitionComponent)
69+
{
70+
if (string.IsNullOrEmpty(emote.DTO.ContentDownloadUrl))
71+
return false;
72+
73+
// Check if emote already has assets loaded or is loading
74+
if (emote.IsLoading)
75+
return true; // Still processing, not done yet
76+
77+
// Check if we already have this emote in storage with assets
78+
if (emoteStorage.TryGetElement(emote.GetUrn(), out IEmote existingEmote))
79+
{
80+
// For unisex emotes with same clip, check if either bodyshape has assets
81+
if (existingEmote.IsUnisex() && existingEmote.HasSameClipForAllGenders())
82+
{
83+
if (existingEmote.AssetResults[BodyShape.MALE] != null || existingEmote.AssetResults[BodyShape.FEMALE] != null)
84+
return false; // Already processed
85+
}
86+
else
87+
{
88+
// For non-unisex emotes, check both bodyshapes
89+
if (existingEmote.AssetResults[BodyShape.MALE] != null && existingEmote.AssetResults[BodyShape.FEMALE] != null)
90+
return false; // Already processed
91+
}
92+
}
93+
94+
bool foundGlb = false;
95+
bool stillProcessing = false;
96+
97+
BodyShape? targetBodyShape = null;
98+
if (!emote.IsUnisex())
99+
targetBodyShape = BodyShape.FromStringSafe(emote.DTO.Metadata.AbstractData.representations[0].bodyShapes[0]);
100+
101+
// The resolution of these promises will be finalized by FinalizeEmoteLoadingSystem
102+
foreach (var content in emote.DTO.content)
103+
{
104+
if (content.file.EndsWith(".glb"))
105+
{
106+
for (int i = 0; i < ALL_BODYSHAPES.Length; i++)
107+
{
108+
BodyShape bodyShape = ALL_BODYSHAPES[i];
109+
if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape!))
110+
continue;
111+
112+
// Skip if this bodyshape already has an asset result
113+
if (emote.AssetResults[bodyShape] != null)
114+
continue;
115+
116+
var gltfPromise = GltfPromise.Create(World!, GetGLTFIntention.Create(content.file, content.hash), partitionComponent);
117+
World!.Create(gltfPromise, emote, bodyShape);
118+
emote.UpdateLoadingStatus(true);
119+
foundGlb = true;
120+
stillProcessing = true;
121+
}
122+
continue;
123+
}
124+
125+
// Supported audio format in emotes: https://docs.decentraland.org/creator/emotes/props-and-sounds/#add-audio-to-the-emotes
126+
if (content.file.EndsWith(".mp3") || content.file.EndsWith(".ogg"))
127+
{
128+
var audioType = content.file.ToAudioType();
129+
urlBuilder.Clear();
130+
urlBuilder.AppendDomain(URLDomain.FromString(emote.DTO.ContentDownloadUrl)).AppendPath(new URLPath(content.hash));
131+
URLAddress url = urlBuilder.Build();
132+
133+
for (int i = 0; i < ALL_BODYSHAPES.Length; i++)
134+
{
135+
BodyShape bodyShape = ALL_BODYSHAPES[i];
136+
if (!emote.IsUnisex() && !bodyShape.Equals(targetBodyShape))
137+
continue;
138+
139+
var audioPromise = AudioUtils.CreateAudioClipPromise(World!, url.Value, audioType, partitionComponent);
140+
World!.Create(audioPromise, emote, bodyShape);
141+
}
142+
143+
if (foundGlb) break;
144+
}
145+
}
146+
147+
return stillProcessing;
148+
}
149+
}
150+
}

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/ResolveBuilderEmotePromisesSystem.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearablePromisesSystem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private void ResolveWearablePromise([Data] bool defaultWearablesResolved, in Ent
6262
{
6363
if (wearablesByPointersIntention.CancellationTokenSource.IsCancellationRequested)
6464
{
65-
World!.Add(entity, new StreamableResult(GetReportCategory(), new Exception("Pointer request cancelled")));
65+
World!.Add(entity, new StreamableResult(GetReportCategory(), new OperationCanceledException("Pointer request cancelled")));
6666
return;
6767
}
6868

Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Cysharp.Threading.Tasks;
55
using DCL.AssetsProvision;
66
using DCL.AvatarRendering.Emotes;
7+
using DCL.AvatarRendering.Emotes.Systems;
78
using DCL.AvatarRendering.Loading.Components;
89
using DCL.AvatarRendering.Wearables;
910
using DCL.Backpack;
@@ -15,14 +16,11 @@
1516
using DCL.Profiles.Self;
1617
using DCL.Web3.Identities;
1718
using DCL.ResourcesUnloading;
18-
using DCL.UI.MainUI;
1919
using DCL.UI.SharedSpaceManager;
2020
using DCL.WebRequests;
2121
using ECS;
2222
using ECS.StreamableLoading.AudioClips;
2323
using ECS.StreamableLoading.Cache;
24-
using ECS.StreamableLoading.GLTF;
25-
using ECS.StreamableLoading.GLTF.DownloadProvider;
2624
using Global.AppArgs;
2725
using MVC;
2826
using System;
@@ -133,6 +131,9 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder<Arch.Core.World> builder,
133131
new NoCache<EmotesResolution, GetOwnedEmotesFromRealmIntention>(false, false),
134132
emoteStorage, builderContentURL);
135133

134+
if(builderCollectionsPreview)
135+
ResolveBuilderEmotePromisesSystem.InjectToWorld(ref builder, emoteStorage);
136+
136137
CharacterEmoteSystem.InjectToWorld(ref builder, emoteStorage, messageBus, audioSourceReference, debugBuilder, localSceneDevelopment, appArgs);
137138

138139
LoadAudioClipGlobalSystem.InjectToWorld(ref builder, audioClipsCache, webRequestController);

0 commit comments

Comments
 (0)