Skip to content

Commit 80bbe7b

Browse files
authored
Merge pull request #6422 from decentraland/hotfix/2025-12-11-fix-social-emote-audio
Fix: Incorrect audio mapping on multiple outcomes (#6406)
2 parents ff8d445 + 125ad83 commit 80bbe7b

14 files changed

Lines changed: 117 additions & 60 deletions

File tree

Explorer/Assets/DCL/AvatarAnimation/Editor/TriggerEmotePlayableBehaviour.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public override void ProcessFrame(Playable playable, FrameData info, object play
4040
profile.IsDirty = true;
4141

4242
// It adds the emote intent (which will be consumed and removed by the CharacterEmoteSystem) if it was not already added
43-
CharacterEmoteIntent emoteIntent = new (){ EmoteId = URN, TriggerSource = TriggerSource.SELF, Spatial = true};
43+
CharacterEmoteIntent emoteIntent = new (URN, triggerSource: TriggerSource.SELF, spatial: true);
4444
GlobalWorld.ECSWorldInstance.Add(cachedEntity, emoteIntent);
4545
}
4646
}

Explorer/Assets/DCL/AvatarRendering/Emotes/Components/CharacterEmoteIntent.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,45 @@ public struct SocialEmoteData
7676
/// </summary>
7777
public bool HasPlayedEmote;
7878

79+
public CharacterEmoteIntent(URN emoteId,
80+
TriggerSource triggerSource = default,
81+
bool spatial = false,
82+
string walletAddress = "",
83+
bool isRepeating = false,
84+
bool useOutcomeAnimation = false,
85+
int outcomeIndex = -1,
86+
bool useOutcomeReactionAnimation = false,
87+
string initiatorWalletAddress = "",
88+
string targetAvatarWalletAddress = "",
89+
int interactionId = 0,
90+
bool isInitiatorOutcomeAnimationWaitingForReceiverAnimationLoop = false)
91+
{
92+
this.WalletAddress = walletAddress;
93+
this.EmoteId = emoteId;
94+
this.Spatial = spatial;
95+
this.TriggerSource = triggerSource;
96+
this.IsRepeating = isRepeating;
97+
this.SocialEmote.UseOutcomeAnimation = useOutcomeAnimation;
98+
this.SocialEmote.OutcomeIndex = outcomeIndex;
99+
this.SocialEmote.UseOutcomeReactionAnimation = useOutcomeReactionAnimation;
100+
this.SocialEmote.InitiatorWalletAddress = initiatorWalletAddress;
101+
this.SocialEmote.TargetAvatarWalletAddress = targetAvatarWalletAddress;
102+
this.SocialEmote.InteractionId = interactionId;
103+
this.SocialEmote.IsInitiatorOutcomeAnimationWaitingForReceiverAnimationLoop = isInitiatorOutcomeAnimationWaitingForReceiverAnimationLoop;
104+
this.EmoteAsset = null;
105+
this.HasPlayedEmote = false;
106+
}
107+
79108
public void UpdateRemoteId(URN emoteId)
80109
{
81-
this.WalletAddress = string.Empty;
110+
this.Reset();
82111
this.EmoteId = emoteId;
112+
}
113+
114+
private void Reset()
115+
{
116+
this.WalletAddress = string.Empty;
117+
this.EmoteId = new URN();
83118
this.Spatial = true;
84119
this.TriggerSource = TriggerSource.REMOTE;
85120
this.IsRepeating = false;

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/FinalizeEmoteLoadingSystem.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,12 @@ private void FinalizeAudioClipPromise(Entity entity, ref IEmote emote, ref Audio
160160

161161
if (result.Succeeded)
162162
{
163-
if (emote.IsSocial && bodyShape.Value == BodyShape.MALE) // Note: We use male body shape only because actually there is only one version of the emote, although there are 2 copies in the metadata due to legacy code, that should be refactored
164-
{
165-
string audioURL = promise.LoadingIntention.CommonArguments.URL.Value;
163+
string audioURL = promise.LoadingIntention.CommonArguments.URL.Value;
164+
165+
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "FinalizeAudioClipPromise() Audio URL: " + audioURL + " body: " + bodyShape.Value);
166166

167+
if (emote.IsSocial)
168+
{
167169
if (emote.SocialEmoteOutcomeAudioAssetResults == null)
168170
{
169171
emote.SocialEmoteOutcomeAudioAssetResults = new StreamableLoadingResult<AudioClipData>?[emote.Model.Asset!.metadata.data!.outcomes!.Length];
@@ -173,13 +175,18 @@ private void FinalizeAudioClipPromise(Entity entity, ref IEmote emote, ref Audio
173175
for (int i = 0; i < emote.Model.Asset!.metadata.data!.outcomes!.Length; ++i)
174176
{
175177
// Several outcomes may have the same audio, in order to avoid setting the same outcome always we skip the already filled slots
176-
if(emote.SocialEmoteOutcomeAudioAssetResults[i].HasValue)
178+
if (emote.SocialEmoteOutcomeAudioAssetResults[i].HasValue)
179+
{
180+
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "FinalizeAudioClipPromise() Next iteration " + i);
177181
continue;
182+
}
178183

179184
if (emote.Model.Asset!.metadata.data!.outcomes![i].audio != null)
180185
{
181186
string? outcomeAudioHash = FindAudioFileHashInContent(emote, bodyShape, emote.Model.Asset!.metadata.data!.outcomes![i].audio);
182187

188+
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "FinalizeAudioClipPromise() outcome audio hash " + outcomeAudioHash);
189+
183190
// If the current result corresponds to the outcome at current position...
184191
if (audioURL.Contains(outcomeAudioHash!, StringComparison.InvariantCultureIgnoreCase))
185192
{
@@ -199,6 +206,8 @@ private void FinalizeAudioClipPromise(Entity entity, ref IEmote emote, ref Audio
199206
{
200207
string? audioHash = FindAudioFileHashInContent(emote, bodyShape, emote.Model.Asset!.metadata.data!.startAnimation!.audio);
201208

209+
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "FinalizeAudioClipPromise() start audio hash " + audioHash);
210+
202211
// If the current result corresponds to the start animation...
203212
if (audioHash != null && audioURL.Contains(audioHash, StringComparison.InvariantCultureIgnoreCase))
204213
{
@@ -211,6 +220,7 @@ private void FinalizeAudioClipPromise(Entity entity, ref IEmote emote, ref Audio
211220
}
212221
else
213222
{
223+
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "FinalizeAudioClipPromise() normal");
214224
emote.AudioAssetResults[bodyShape] = result;
215225
}
216226
}

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Play/CharacterEmoteSystem.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ protected override void Update(float t)
117117
AfterPlayingUpdateSocialEmoteInteractionsQuery(World);
118118
RotateReceiverAvatarToCoincideWithInitiatorAvatarQuery(World); // This must occur after ConsumeEmoteIntentQuery, because it has to rotate the avatar one frame after the emote plays, at least
119119
ConsumeStopEmoteIntentQuery(World); // Repeated on purpose, if the state of both participants in a social emote interaction must be consistent all the time
120-
CancelEmotesByDeletionQuery(World);
120+
CancelEmotesByDeletionWhenProfileIsNotPresentQuery(World);
121+
CancelEmotesByDeletionWhenProfileIsPresentQuery(World);
121122
UpdateEmoteTagsQuery(World);
122123
DisableCharacterControllerQuery(World);
123124
DisableAnimatorWhenPlayingLegacyAnimationsQuery(World);
@@ -134,7 +135,15 @@ private void AvatarStateMachineEventHandlerInitialization(Entity entity, IAvatar
134135

135136
[Query]
136137
[All(typeof(DeleteEntityIntention))]
137-
private void CancelEmotesByDeletion(Entity entity, ref CharacterEmoteComponent emoteComponent, in IAvatarView avatarView, in Profile profile)
138+
[None(typeof(Profile))]
139+
private void CancelEmotesByDeletionWhenProfileIsNotPresent(Entity entity, ref CharacterEmoteComponent emoteComponent, in IAvatarView avatarView)
140+
{
141+
StopEmote(entity, ref emoteComponent, avatarView, string.Empty);
142+
}
143+
144+
[Query]
145+
[All(typeof(DeleteEntityIntention), typeof(Profile))]
146+
private void CancelEmotesByDeletionWhenProfileIsPresent(Entity entity, ref CharacterEmoteComponent emoteComponent, in IAvatarView avatarView, in Profile profile)
138147
{
139148
StopEmote(entity, ref emoteComponent, avatarView, profile.UserId);
140149
}
@@ -577,7 +586,7 @@ private void AfterPlayingUpdateSocialEmoteInteractions(Entity entity, ref Charac
577586
if(emoteIntent.EmoteAsset == null || !emoteIntent.HasPlayedEmote)
578587
return;
579588

580-
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "AfterPlayingUpdateSocialEmoteInteractions()");
589+
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "AfterPlayingUpdateSocialEmoteInteractions() wallet: " + emoteIntent.WalletAddress + " emote: " + emoteIntent.EmoteId);
581590

582591
// it's very important to catch any exception here to avoid not consuming the emote intent, so we don't infinitely create props
583592
try

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/SocialEmoteInteractionSystem.cs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,17 @@ private void PlayInitiatorOutcomeAnimation(Entity entity, Profile profile, ref C
6262
{
6363
ReportHub.Log(ReportCategory.SOCIAL_EMOTE, "PlayInitiatorOutcomeAnimation() CharacterEmoteIntent Initiator outcome animation " + profile.UserId);
6464

65-
World.Add(entity, new CharacterEmoteIntent()
66-
{
67-
EmoteId = socialEmoteInteraction.Emote.DTO.Metadata.id!,
68-
TriggerSource = TriggerSource.SELF,
69-
Spatial = true,
70-
WalletAddress = profile.UserId,
71-
SocialEmote = new CharacterEmoteIntent.SocialEmoteData()
72-
{
73-
OutcomeIndex = socialEmoteInteraction.OutcomeIndex,
74-
UseOutcomeReactionAnimation = false,
75-
InitiatorWalletAddress = profile.UserId,
76-
UseOutcomeAnimation = true,
77-
InteractionId = socialEmoteInteraction.Id
78-
}
79-
});
65+
World.Add(entity, new CharacterEmoteIntent(
66+
socialEmoteInteraction.Emote.DTO.Metadata.id!,
67+
triggerSource : TriggerSource.SELF,
68+
spatial : true,
69+
walletAddress : profile.UserId,
70+
outcomeIndex : socialEmoteInteraction.OutcomeIndex,
71+
useOutcomeReactionAnimation : false,
72+
initiatorWalletAddress : profile.UserId,
73+
useOutcomeAnimation : true,
74+
interactionId : socialEmoteInteraction.Id
75+
));
8076
}
8177
}
8278

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/UpdateEmoteInputSystem.cs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private void TriggerEmote([Data] int emoteIndex, [Data] string emoteUrn, in Enti
9292
}
9393
else // Normal emotes, or social emote start animation
9494
{
95-
if(inputModifier.DisableEmote || !avatarShapeComponent.IsVisible)
95+
if (inputModifier.DisableEmote)
9696
return;
9797

9898
IReadOnlyList<URN> emotes = profile.Avatar.Emotes;
@@ -123,21 +123,18 @@ private void SendEmoteMessage(URN emoteId,
123123
return;
124124

125125
var newEmoteIntent = new CharacterEmoteIntent
126-
{
127-
EmoteId = emoteId,
128-
Spatial = true,
129-
TriggerSource = TriggerSource.SELF,
130-
WalletAddress = walletAddress,
131-
SocialEmote = new CharacterEmoteIntent.SocialEmoteData()
132-
{
133-
OutcomeIndex = socialEmoteOutcomeIndex,
134-
UseOutcomeReactionAnimation = useOutcomeReactionAnimation,
135-
UseOutcomeAnimation = useSocialEmoteOutcomeAnimation,
136-
InitiatorWalletAddress = socialEmoteInitiatorWalletAddress,
137-
TargetAvatarWalletAddress = targetAvatarWalletAddress,
138-
InteractionId = socialEmoteInteractionId
139-
}
140-
};
126+
(
127+
emoteId,
128+
triggerSource: TriggerSource.SELF,
129+
spatial: true,
130+
walletAddress : walletAddress,
131+
outcomeIndex : socialEmoteOutcomeIndex,
132+
useOutcomeReactionAnimation : useOutcomeReactionAnimation,
133+
useOutcomeAnimation : useSocialEmoteOutcomeAnimation,
134+
initiatorWalletAddress : socialEmoteInitiatorWalletAddress,
135+
targetAvatarWalletAddress : targetAvatarWalletAddress,
136+
interactionId : socialEmoteInteractionId
137+
);
141138
ref var emoteIntent = ref World.AddOrGet(entity, newEmoteIntent);
142139
emoteIntent = newEmoteIntent;
143140

Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public bool IsAvatarLoaded() =>
151151

152152
public void PlayEmote(string emoteId)
153153
{
154-
var intent = new CharacterEmoteIntent { EmoteId = emoteId, TriggerSource = TriggerSource.PREVIEW };
154+
var intent = new CharacterEmoteIntent(emoteId, triggerSource: TriggerSource.PREVIEW);
155155

156156
if (globalWorld.Has<CharacterEmoteIntent>(characterPreviewEntity))
157157
globalWorld.Set(characterPreviewEntity, intent);

Explorer/Assets/DCL/Communities/CommunitiesCard/Events/EventListController.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,16 @@ protected override async UniTask<int> FetchDataAsync(CancellationToken ct)
217217
placeInfoCache.Add(place.id, place);
218218

219219
foreach (var item in eventResponse.Value.data.events)
220+
{
221+
if (!placeInfoCache.TryGetValue(item.place_id, out PlaceInfo? place))
222+
continue;
223+
220224
eventsFetchData.Items.Add(new PlaceAndEventDTO
221225
{
222-
Place = placeInfoCache[item.place_id],
226+
Place = place,
223227
Event = item
224228
});
229+
}
225230

226231
return eventResponse.Value.data.total;
227232
}

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/GlobalWorldActions.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,11 @@ public void RotateCamera(Vector3? newCameraTarget, Vector3 newPlayerPosition)
7878

7979
public void TriggerEmote(URN urn, bool isLooping)
8080
{
81-
if (world.TryGet(playerEntity, out AvatarShapeComponent avatarShape) && !avatarShape.IsVisible) return;
81+
//TODO (Juani Emotes Refactor): Re-analyze this if, probably remove it
82+
if (!world.TryGet(playerEntity, out AvatarShapeComponent avatarShape)) return;
8283

8384
// If it's just Add() there are inconsistencies when the intent is processed at CharacterEmoteSystem for rapidly triggered emotes...
84-
world.AddOrSet(playerEntity, new CharacterEmoteIntent { EmoteId = urn, Spatial = true, TriggerSource = TriggerSource.SCENE });
85+
world.AddOrSet(playerEntity, new CharacterEmoteIntent (urn, triggerSource: TriggerSource.SCENE, spatial: true ));
8586

8687
messageBus.Send(urn, isLooping, false, -1, false, string.Empty, string.Empty, false, 0);
8788
}
@@ -115,8 +116,12 @@ await TriggerSceneEmoteFromRealmAsync(
115116

116117
private async UniTask TriggerSceneEmoteFromRealmAsync(string sceneId, AssetBundleManifestVersion sceneAssetBundleManifestVersion, string emoteHash, bool loop, CancellationToken ct)
117118
{
118-
if (!world.TryGet(playerEntity, out AvatarShapeComponent avatarShape))
119-
throw new Exception("Cannot resolve body shape of current player because its missing AvatarShapeComponent");
119+
//TODO (Juani Emotes Refactor): Re-analyze this if, probably remove it.
120+
// I left the previous behaviour here, just in case we want to consider it
121+
if (!world.TryGet(playerEntity, out AvatarShapeComponent avatarShape)) return;
122+
123+
//if (!world.TryGet(playerEntity, out AvatarShapeComponent avatarShape))
124+
// throw new Exception("Cannot resolve body shape of current player because its missing AvatarShapeComponent");
120125

121126
if (!avatarShape.IsVisible) return;
122127

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/GlobalWorldActionsShould.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@ public void AddIntentAndSendMessageWhenAvatarVisible()
140140
}
141141

142142
[Test]
143-
public void DoNothingWhenAvatarNotVisible()
143+
public void CanPlayEmotesWhenAvatarNotVisible()
144144
{
145145
world.Add(playerEntity, new AvatarShapeComponent { IsVisible = false });
146146
var emoteUrn = new URN("urn:emote:id");
147147

148148
globalWorldActions.TriggerEmote(emoteUrn, false);
149149

150-
Assert.IsFalse(world.Has<CharacterEmoteIntent>(playerEntity));
151-
Assert.AreEqual(0, mockMessageBus.SentEmotes.Count);
150+
Assert.IsTrue(world.Has<CharacterEmoteIntent>(playerEntity));
151+
Assert.AreEqual(1, mockMessageBus.SentEmotes.Count);
152152
}
153153

154154
[Test]

0 commit comments

Comments
 (0)