Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,20 @@
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<ECS.StreamableLoading.GLTF.GLTFData, ECS.StreamableLoading.GLTF.GetGLTFIntention>;

namespace DCL.AvatarRendering.Emotes.Load
{
[UpdateInGroup(typeof(PresentationSystemGroup))]
[LogCategory(ReportCategory.EMOTE)]
public partial class LoadOwnedEmotesSystem : LoadElementsByIntentionSystem<EmotesResolution, GetOwnedEmotesFromRealmIntention, IEmote, EmoteDTO>
{
private static readonly BodyShape[] ALL_BODYSHAPES = { BodyShape.MALE, BodyShape.FEMALE };

internal IURLBuilder urlBuilder = new URLBuilder();
private readonly IEmoteStorage emoteStorage;

public LoadOwnedEmotesSystem(
World world,
Expand All @@ -36,7 +28,6 @@ public LoadOwnedEmotesSystem(
string? builderContentURL = null
) : base(world, cache, emoteStorage, webRequestController, realmData, builderContentURL, "emote")
{
this.emoteStorage = emoteStorage;
}

protected override async UniTask<IAttachmentLambdaResponse<ILambdaResponseElement<EmoteDTO>>> ParseResponseAsync(GenericDownloadHandlerUtils.Adapter<GenericGetRequest, GenericGetArguments> adapter) =>
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DCL.AvatarRendering.Emotes.EmotesResolution>;
using GltfPromise = ECS.StreamableLoading.Common.AssetPromise<ECS.StreamableLoading.GLTF.GLTFData, ECS.StreamableLoading.GLTF.GetGLTFIntention>;

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 OperationCanceledException("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;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
7 changes: 4 additions & 3 deletions Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -133,6 +131,9 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder<Arch.Core.World> builder,
new NoCache<EmotesResolution, GetOwnedEmotesFromRealmIntention>(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);
Expand Down
Loading