-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathSceneMaskedEmoteSystem.cs
More file actions
329 lines (263 loc) · 13.6 KB
/
Copy pathSceneMaskedEmoteSystem.cs
File metadata and controls
329 lines (263 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
using Arch.Core;
using Arch.System;
using Arch.SystemGroups;
using CommunicationData.URLHelpers;
using DCL.AvatarRendering.AvatarShape.Components;
using DCL.AvatarRendering.AvatarShape.UnityInterface;
using DCL.AvatarRendering.Loading.Assets;
using DCL.AvatarRendering.Loading.Components;
using DCL.CharacterMotion.Components;
using DCL.Diagnostics;
using DCL.Multiplayer.Emotes;
using DCL.Utilities;
using ECS.Abstract;
using ECS.Groups;
using ECS.LifeCycle;
using ECS.StreamableLoading.AudioClips;
using ECS.StreamableLoading.Common.Components;
using SceneRunner.Scene;
using UnityEngine;
using Utility.Animations;
namespace DCL.AvatarRendering.Emotes.Play
{
[LogCategory(ReportCategory.EMOTE)]
[UpdateInGroup(typeof(SyncedPresentationSystemGroup))]
public partial class SceneMaskedEmoteSystem : BaseUnityLoopSystem, IFinalizeWorldSystem
{
private readonly World globalWorld;
private readonly Entity globalPlayerEntity;
private readonly ObjectProxy<AvatarBase> mainPlayerAvatarBaseProxy;
private readonly EmotePlayer emotePlayer;
private readonly IEmoteStorage emoteStorage;
private readonly IEmotesMessageBus messageBus;
private readonly ISceneStateProvider sceneStateProvider;
internal SceneMaskedEmoteSystem(
World world,
World globalWorld,
Entity globalPlayerEntity,
ObjectProxy<AvatarBase> mainPlayerAvatarBaseProxy,
EmotePlayer emotePlayer,
IEmoteStorage emoteStorage,
IEmotesMessageBus messageBus,
ISceneStateProvider sceneStateProvider) : base(world)
{
this.globalWorld = globalWorld;
this.globalPlayerEntity = globalPlayerEntity;
this.mainPlayerAvatarBaseProxy = mainPlayerAvatarBaseProxy;
this.emotePlayer = emotePlayer;
this.emoteStorage = emoteStorage;
this.messageBus = messageBus;
this.sceneStateProvider = sceneStateProvider;
}
protected override void Update(float t)
{
if (!mainPlayerAvatarBaseProxy.Configured) return;
CancelMaskedEmotesQuery(World);
ConsumeMaskedEmoteIntentQuery(World, t);
UpdateMaskedEmoteVisibilityQuery(World);
ReplicateLoopingMaskedEmotesQuery(World);
UpdateMaskedEmoteTagsQuery(World);
}
[Query]
[All(typeof(CharacterMaskedEmoteComponent))]
private void FinalizeComponents(ref CharacterMaskedEmoteComponent masked)
{
if (masked.CurrentEmoteReference != null)
TryStopMaskedEmote(ref masked);
masked.Reset();
}
public void FinalizeComponents(in Query query) =>
FinalizeComponentsQuery(World);
[Query]
private void CancelMaskedEmotes(ref CharacterMaskedEmoteComponent masked)
{
if (masked.StopEmote)
{
TryStopMaskedEmote(ref masked, permanent: true);
return;
}
if (masked.CurrentEmoteReference == null) return;
AvatarBase view = mainPlayerAvatarBaseProxy.Object!;
// Both branches below tear down an emote that stopped by itself. A non-looping clip ran to its
// end and has nothing left to resume, so its urn is cleared too; a looping one lost its animator
// state to something else and keeps the urn to be resumed.
// Legacy-blender path: tear down once the blender signals natural completion.
if (view.HasMaskedLegacyEmoteFinished)
{
TryStopMaskedEmote(ref masked, permanent: !masked.EmoteLoop);
return;
}
if (view.IsMaskedLegacyEmotePlaying) return;
if (!masked.IsPlaying) return;
string layer = AnimatorEmoteLayers.GetFromEmoteMask(masked.Mask);
int currentTag = view.GetAnimatorCurrentStateTag(layer);
bool isOnAnotherTag = currentTag != AnimationHashes.MASKED_EMOTE && currentTag != AnimationHashes.MASKED_EMOTE_LOOP;
if (isOnAnotherTag)
TryStopMaskedEmote(ref masked, permanent: !masked.EmoteLoop);
}
[Query]
private void ConsumeMaskedEmoteIntent([Data] float dt, Entity entity,
ref CharacterMaskedEmoteComponent masked,
ref CharacterEmoteIntent emoteIntent)
{
URN emoteId = emoteIntent.EmoteId;
if (!emoteStorage.TryGetElement(emoteId.Shorten(), out IEmote emote)) return;
if (emote.IsLoading)
return;
if (emote.Model is { IsInitialized: true, Succeeded: false })
{
World.Remove<CharacterEmoteIntent>(entity);
return;
}
if (emoteIntent.UpdatePlayTimeout(dt))
{
ReportHub.LogError(GetReportData(), $"Cant play masked emote {emoteId} timeout reached.");
World.Remove<CharacterEmoteIntent>(entity);
return;
}
if (!globalWorld.TryGet(globalPlayerEntity, out AvatarShapeComponent avatarShapeComponent))
{
World.Remove<CharacterEmoteIntent>(entity);
return;
}
BodyShape bodyShape = avatarShapeComponent.BodyShape;
if (emote.AssetResults[bodyShape] == null)
return;
StreamableLoadingResult<AttachmentRegularAsset> streamableAssetValue = emote.AssetResults[bodyShape]!.Value;
GameObject? mainAsset;
if (streamableAssetValue is { Succeeded: false } || (mainAsset = streamableAssetValue.Asset?.MainAsset) == null)
{
World.Remove<CharacterEmoteIntent>(entity);
return;
}
StreamableLoadingResult<AudioClipData>? audioAssetResult = emote.AudioAssetResults[bodyShape];
AudioClip? audioClip = audioAssetResult?.Asset;
// Stop any full-body emote that's playing on the global player
if (globalWorld.TryGet(globalPlayerEntity, out CharacterEmoteComponent emoteComponent) && emoteComponent.CurrentEmoteReference != null)
{
emoteComponent.StopEmote = true;
globalWorld.Set(globalPlayerEntity, emoteComponent);
}
// Stop previous masked emote if one exists
if (masked.CurrentEmoteReference != null)
TryStopMaskedEmote(ref masked);
masked.EmoteUrn = emoteId;
masked.Mask = emoteIntent.Mask;
if (!mainPlayerAvatarBaseProxy.Configured) return;
IAvatarView avatarBase = mainPlayerAvatarBaseProxy.Object!;
bool isLooping = emote.IsLooping();
if (!emotePlayer.PlayMasked(mainAsset, audioClip, isLooping, emoteIntent.Spatial, in avatarBase, ref masked))
ReportHub.LogError(ReportCategory.EMOTE, $"Emote name:{emoteId} cant be played.");
else
{
// The duration comes from the masked emote's own clip; a looping emote has no end to report.
uint durationMs = !isLooping ? (uint)(masked.PlayingEmoteDuration * 1000) : 0;
globalWorld.Add(globalPlayerEntity, new EmotePendingToBroadcast { EmoteId = emoteId, DurationMs = durationMs, Mask = emoteIntent.Mask });
}
World.Remove<CharacterEmoteIntent>(entity);
}
[Query]
private void UpdateMaskedEmoteVisibility(ref CharacterMaskedEmoteComponent masked)
{
if (masked.EmoteUrn.IsNullOrEmpty()) return;
bool isInScene = sceneStateProvider.IsCurrent;
bool fullBodyIsPlaying = globalWorld.TryGet(globalPlayerEntity, out CharacterEmoteComponent ec)
&& ec.CurrentEmoteReference != null;
bool isGliding = globalWorld.TryGet(globalPlayerEntity, out GlideState glideState)
&& glideState.Value is GlideStateValue.OpeningProp or GlideStateValue.Gliding;
bool shouldPlay = isInScene && !fullBodyIsPlaying && !isGliding;
if (shouldPlay && masked.CurrentEmoteReference == null)
ReplayMaskedEmote(ref masked);
else if (!shouldPlay && masked.CurrentEmoteReference != null)
TryStopMaskedEmote(ref masked);
}
private void ReplayMaskedEmote(ref CharacterMaskedEmoteComponent masked)
{
if (!emoteStorage.TryGetElement(masked.EmoteUrn.Shorten(), out IEmote emote)) return;
if (emote.IsLoading) return;
// Replaying resumes a looping emote that was suspended. A non-looping emote has already
// delivered its single playback, so drop the urn instead of restarting it on every frame
// that the play conditions hold.
if (!emote.IsLooping())
{
masked.Reset();
return;
}
if (!globalWorld.TryGet(globalPlayerEntity, out AvatarShapeComponent avatarShape)) return;
BodyShape bodyShape = avatarShape.BodyShape;
if (emote.AssetResults[bodyShape] == null) return;
StreamableLoadingResult<AttachmentRegularAsset> streamableAssetValue = emote.AssetResults[bodyShape]!.Value;
if (streamableAssetValue is { Succeeded: false } || streamableAssetValue.Asset?.MainAsset == null) return;
GameObject mainAsset = streamableAssetValue.Asset.MainAsset;
StreamableLoadingResult<AudioClipData>? audioAssetResult = emote.AudioAssetResults[bodyShape];
AudioClip? audioClip = audioAssetResult?.Asset;
if (!mainPlayerAvatarBaseProxy.Configured) return;
IAvatarView avatarBase = mainPlayerAvatarBaseProxy.Object!;
if (!emotePlayer.PlayMasked(mainAsset, audioClip, isLooping: true, isSpatial: true, in avatarBase, ref masked))
return;
// Reset stored tag so CancelMaskedEmotes doesn't fire on the next frame
// before UpdateMaskedEmoteTags has a chance to set the real animator state.
masked.SetAnimationTag(0);
// Only looping emotes are replayed, and a looping emote has no end to report.
globalWorld.Add(globalPlayerEntity, new EmotePendingToBroadcast { EmoteId = masked.EmoteUrn, Mask = masked.Mask });
}
/// <summary>
/// Re-broadcasts the masked emote on every loop cycle so that late-joining
/// players receive the message and can start playing the emote.
/// Mirrors ReplicateLoopingEmotes in CharacterEmoteSystem for full-body emotes.
/// Must run before UpdateMaskedEmoteTags so it can detect the tag transition.
/// </summary>
// TODO: we can safely remove this propagation for pulse multiplayer as it is no longer needed (based on emote start/emote stop events)
[Query]
[None(typeof(CharacterEmoteIntent))]
private void ReplicateLoopingMaskedEmotes(ref CharacterMaskedEmoteComponent masked)
{
int prevTag = masked.CurrentAnimationTag;
if (prevTag == 0) return;
string layer = AnimatorEmoteLayers.GetFromEmoteMask(masked.Mask);
int currentTag = mainPlayerAvatarBaseProxy.Object!.GetAnimatorCurrentStateTag(layer);
if ((prevTag != AnimationHashes.MASKED_EMOTE || currentTag != AnimationHashes.MASKED_EMOTE_LOOP)
&& (prevTag != AnimationHashes.MASKED_EMOTE_LOOP || currentTag != AnimationHashes.MASKED_EMOTE)) return;
messageBus.Send(masked.EmoteUrn, true, masked.Mask);
}
[Query]
private void UpdateMaskedEmoteTags(ref CharacterMaskedEmoteComponent masked) =>
EmotePlayer.UpdateMaskedEmoteTag(ref masked, mainPlayerAvatarBaseProxy.Object!);
/// <summary>
/// Stops the masked emote animation and releases pool references.
/// When permanent=true, clears EmoteUrn (no replay possible).
/// When permanent=false, preserves EmoteUrn/Mask for replay on re-entry.
/// </summary>
private void TryStopMaskedEmote(ref CharacterMaskedEmoteComponent masked, bool permanent = false)
{
// If a full-body emote is already playing on the local player, its EmoteStart has
// already superseded this masked emote on the server (EmoteStop carries no emote id,
// so sending it now would clear that emote instead).
bool anotherEmoteIsBroadcast = globalWorld.TryGet(globalPlayerEntity, out CharacterEmoteComponent emoteComponent)
&& emoteComponent.CurrentEmoteReference != null;
if (!anotherEmoteIsBroadcast)
messageBus.SendStop();
if (masked.CurrentEmoteReference == null)
{
if (permanent)
masked.Reset();
return;
}
if (!mainPlayerAvatarBaseProxy.Configured) return;
IAvatarView avatarBase = mainPlayerAvatarBaseProxy.Object!;
emotePlayer.StopMasked(masked.CurrentEmoteReference, in avatarBase, masked.Mask);
if (permanent)
masked.Reset();
else
{
// Keep EmoteUrn and Mask so we can replay when re-entering the scene.
// Reset the stored animation tag to prevent CancelMaskedEmotes from
// erroneously firing with a stale IsPlaying=true on the next frame.
masked.CurrentEmoteReference = null;
masked.EmoteLoop = false;
masked.StopEmote = false;
masked.SetAnimationTag(0);
}
}
}
}