< Summary

Class:DCL.Emotes.EmoteAnimationLoader
Assembly:EmotesService
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/DCLServices/EmotesService/EmoteAnimationLoader.cs
Covered lines:0
Uncovered lines:76
Coverable lines:76
Total lines:168
Line coverage:0% (0 of 76)
Covered branches:0
Total branches:0
Covered methods:0
Total methods:14
Method coverage:0% (0 of 14)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
EmoteAnimationLoader(...)0%2100%
LoadRemoteEmote()0%3421800%
LoadLocalEmote()0%20400%
SetupEmote(...)0%90900%
SetupAudioClip()0%12300%
IsValidAudioClip(...)0%6200%
AsyncLoadAudioClip()0%12300%
Dispose()0%6200%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/DCLServices/EmotesService/EmoteAnimationLoader.cs

#LineLine coverage
 1using AvatarSystem;
 2using Cysharp.Threading.Tasks;
 3using DCL.Helpers;
 4using DCL.Providers;
 5using DCLServices.EmotesService;
 6using System;
 7using System.Threading;
 8using UnityEngine;
 9using Object = UnityEngine.Object;
 10
 11namespace DCL.Emotes
 12{
 13    public class EmoteAnimationLoader : IEmoteAnimationLoader
 14    {
 15        private const string EMOTE_AUDIO_SOURCE = "EmoteAudioSource";
 16        private readonly IWearableRetriever retriever;
 17        private readonly AddressableResourceProvider resourceProvider;
 018        public AnimationClip mainClip { get; private set; }
 019        public GameObject container { get; private set; }
 020        public AudioSource audioSource { get; private set; }
 21
 22        private AudioClip audioClip;
 23        private AssetPromise_AudioClip audioClipPromise;
 24        private readonly EmoteVolumeHandler emoteVolumeHandler;
 25
 026        public EmoteAnimationLoader(IWearableRetriever retriever, AddressableResourceProvider resourceProvider, EmoteVol
 27        {
 028            this.emoteVolumeHandler = emoteVolumeHandler;
 029            this.retriever = retriever;
 030            this.resourceProvider = resourceProvider;
 031        }
 32
 33        public async UniTask LoadRemoteEmote(GameObject targetContainer, WearableItem emote, string bodyShapeId, Cancell
 34        {
 035            if (targetContainer == null)
 036                throw new NullReferenceException("Container cannot be null");
 37
 038            if (emote == null)
 039                throw new NullReferenceException("Emote cannot be null");
 40
 041            if (string.IsNullOrEmpty(bodyShapeId))
 042                throw new NullReferenceException("bodyShapeId cannot be null or empty");
 43
 044            ct.ThrowIfCancellationRequested();
 45
 046            Rendereable rendereable = await retriever.Retrieve(targetContainer, emote, bodyShapeId, ct);
 47
 048            foreach (Renderer renderer in rendereable.renderers)
 049                renderer.enabled = false;
 50
 051            GameObject emoteInstance = rendereable.container;
 52
 053            SetupEmote(emoteInstance, emote.id);
 54
 055            var contentProvider = emote.GetContentProvider(bodyShapeId);
 56
 057            foreach (var contentMap in contentProvider.contents)
 58            {
 059                if (!IsValidAudioClip(contentMap.file)) continue;
 60
 061                AudioClip audioClip = null;
 62                try
 63                {
 064                    audioClip = await AsyncLoadAudioClip(contentMap.file, contentProvider);
 065                }
 66                catch (Exception e)
 67                {
 068                    Debug.LogError(e);
 069                }
 70
 071                if (audioClip != null)
 072                    await SetupAudioClip(audioClip, emoteInstance, ct);
 73
 74                // we only support one audio clip
 075                break;
 76            }
 77
 078        }
 79
 80        public async UniTask LoadLocalEmote(GameObject targetContainer, ExtendedEmote embeddedEmote, CancellationToken c
 81        {
 082            GameObject emoteInstance = Object.Instantiate(embeddedEmote.propPrefab, targetContainer.transform, false);
 083            var renderers = emoteInstance.GetComponentsInChildren<Renderer>(true);
 084            foreach (Renderer renderer in renderers)
 085                renderer.enabled = false;
 86
 087            SetupEmote(emoteInstance, embeddedEmote.id);
 088            await SetupAudioClip(embeddedEmote.clip, emoteInstance, ct);
 089        }
 90
 91        private void SetupEmote(GameObject emoteInstance, string emoteId)
 92        {
 093            var animation = emoteInstance.GetComponentInChildren<Animation>();
 94
 095            if (animation == null)
 96            {
 097                Debug.LogError("Animation component not found in the container for emote " + emoteId);
 098                return;
 99            }
 100
 0101            mainClip = animation.clip;
 102
 0103            if (animation.GetClipCount() > 1)
 104            {
 0105                this.container = emoteInstance;
 106
 107                // we cant use the animation order so we use the naming convention at /creator/emotes/props-and-sounds/
 0108                foreach (AnimationState state in animation)
 109                {
 0110                    AnimationClip clip = state.clip;
 111
 112                    // Replace the main clip with the one that's correctly named
 0113                    if (clip.name.Contains("_avatar", StringComparison.OrdinalIgnoreCase) || clip.name == emoteId)
 0114                        mainClip = clip;
 115
 116                    // There's a bug with the legacy animation where animations start ahead of time the first time
 117                    // our workaround is to play every animation while we load the audio clip and then disable the anima
 0118                    animation.Play(clip.name);
 119                }
 120            }
 121
 0122            if (mainClip == null)
 123            {
 0124                Debug.LogError("AnimationClip not found in the container for emote " + emoteId);
 0125                return;
 126            }
 127
 128            // Clip names should be unique because of the Legacy Animation string based usage.
 129            // In rare cases some animations might use the same GLB, thus causing this clip to be used by 2 different em
 130            //     so we avoid renaming the clip again witch can cause problems as we use the clip names internally
 0131            if (!mainClip.name.Contains("urn"))
 0132                mainClip.name = emoteId;
 133
 0134            animation.Stop();
 0135            animation.enabled = false;
 0136        }
 137
 138        private async UniTask SetupAudioClip(AudioClip clip, GameObject audioSourceParent, CancellationToken ct)
 139        {
 0140            audioClip = clip;
 0141            audioSource = await resourceProvider.Instantiate<AudioSource>(EMOTE_AUDIO_SOURCE, "EmoteAudioSource",
 142                cancellationToken: ct);
 0143            audioSource.clip = audioClip;
 0144            audioSource.transform.SetParent(audioSourceParent.transform, false);
 0145            audioSource.transform.ResetLocalTRS();
 146
 0147            emoteVolumeHandler.AddAudioSource(audioSource);
 0148        }
 149
 150        private bool IsValidAudioClip(string fileName) =>
 0151            fileName.EndsWith(".ogg") || fileName.EndsWith(".mp3");
 152
 153        private async UniTask<AudioClip> AsyncLoadAudioClip(string file, ContentProvider contentProvider)
 154        {
 0155            audioClipPromise = new AssetPromise_AudioClip(file, contentProvider);
 0156            AssetPromiseKeeper_AudioClip.i.Keep(audioClipPromise);
 0157            await audioClipPromise;
 0158            return audioClipPromise.asset.audioClip;
 0159        }
 160
 161        public void Dispose()
 162        {
 0163            emoteVolumeHandler.RemoveAudioSource(audioSource);
 0164            AssetPromiseKeeper_AudioClip.i.Forget(audioClipPromise);
 0165            retriever?.Dispose();
 0166        }
 167    }
 168}