| | 1 | | using Cysharp.Threading.Tasks; |
| | 2 | | using DCL; |
| | 3 | | using DCLServices.Lambdas; |
| | 4 | | using MainScripts.DCL.Helpers.Utils; |
| | 5 | | using System; |
| | 6 | | using System.Collections.Generic; |
| | 7 | | using System.Linq; |
| | 8 | | using System.Threading; |
| | 9 | | using UnityEngine.Pool; |
| | 10 | |
|
| | 11 | | namespace DCLServices.EmotesCatalog |
| | 12 | | { |
| | 13 | | public class EmotesBatchRequest : IEmotesRequestSource |
| | 14 | | { |
| | 15 | | [Serializable] |
| | 16 | | public class OwnedEmotesRequestDto |
| | 17 | | { |
| | 18 | | // https://decentraland.github.io/catalyst-api-specs/#tag/Lambdas/operation/getEmotes |
| | 19 | |
|
| | 20 | | [Serializable] |
| | 21 | | public class EmoteRequestDto |
| | 22 | | { |
| | 23 | | public string urn; |
| | 24 | | public int amount; |
| | 25 | | public IndividualData[] individualData; |
| | 26 | |
|
| | 27 | | [Serializable] |
| | 28 | | public struct IndividualData |
| | 29 | | { |
| | 30 | | // extended urn |
| | 31 | | public string id; |
| | 32 | | } |
| | 33 | | } |
| | 34 | |
|
| | 35 | | public EmoteRequestDto[] elements; |
| | 36 | | public int totalAmount; |
| | 37 | | public int pageSize; |
| | 38 | | public int pageNum; |
| | 39 | | } |
| | 40 | |
|
| | 41 | | public event Action<IReadOnlyList<WearableItem>> OnEmotesReceived; |
| | 42 | | public event IEmotesRequestSource.OwnedEmotesReceived OnOwnedEmotesReceived; |
| | 43 | | public event EmoteRejectedDelegate OnEmoteRejected; |
| | 44 | |
|
| | 45 | | private readonly ILambdasService lambdasService; |
| | 46 | | private readonly ICatalyst catalyst; |
| 40 | 47 | | private readonly HashSet<string> pendingRequests = new (); |
| | 48 | | private readonly CancellationTokenSource cts; |
| | 49 | | private readonly BaseVariable<FeatureFlag> featureFlags; |
| | 50 | | private UniTaskCompletionSource<IReadOnlyList<WearableItem>> lastRequestSource; |
| | 51 | |
|
| 0 | 52 | | private string assetBundlesUrl => featureFlags.Get().IsFeatureEnabled("ab-new-cdn") ? "https://ab-cdn.decentrala |
| | 53 | |
|
| 40 | 54 | | public EmotesBatchRequest(ILambdasService lambdasService, IServiceProviders serviceProviders, BaseVariable<Featu |
| | 55 | | { |
| 40 | 56 | | this.featureFlags = featureFlags; |
| 40 | 57 | | this.lambdasService = lambdasService; |
| 40 | 58 | | catalyst = serviceProviders.catalyst; |
| 40 | 59 | | cts = new CancellationTokenSource(); |
| 40 | 60 | | } |
| | 61 | |
|
| | 62 | | public void Dispose() |
| | 63 | | { |
| 0 | 64 | | cts.Cancel(); |
| 0 | 65 | | cts.Dispose(); |
| 0 | 66 | | } |
| | 67 | |
|
| | 68 | | public void RequestOwnedEmotes(string userId) |
| | 69 | | { |
| 0 | 70 | | RequestOwnedEmotesAsync(userId).Forget(); |
| 0 | 71 | | } |
| | 72 | |
|
| | 73 | | private async UniTask RequestOwnedEmotesAsync(string userId) |
| | 74 | | { |
| 0 | 75 | | var url = $"{catalyst.lambdasUrl}/users/{userId}/emotes"; |
| | 76 | |
|
| 0 | 77 | | var requestedEmotes = new List<OwnedEmotesRequestDto.EmoteRequestDto>(); |
| | 78 | |
|
| 0 | 79 | | if (!await FullListEmoteFetch(userId, url, requestedEmotes)) |
| 0 | 80 | | return; |
| | 81 | |
|
| 0 | 82 | | PoolUtils.ListPoolRent<string> tempList = PoolUtils.RentList<string>(); |
| 0 | 83 | | List<string> emoteUrns = tempList.GetList(); |
| | 84 | |
|
| 0 | 85 | | var urnToAmountMap = new Dictionary<string, int>(); |
| 0 | 86 | | var idToExtendedUrn = new Dictionary<string, string>(); |
| | 87 | |
|
| 0 | 88 | | foreach (OwnedEmotesRequestDto.EmoteRequestDto emoteRequestDto in requestedEmotes) |
| | 89 | | { |
| 0 | 90 | | emoteUrns.Add(emoteRequestDto.urn); |
| 0 | 91 | | urnToAmountMap[emoteRequestDto.urn] = emoteRequestDto.amount; |
| 0 | 92 | | idToExtendedUrn[emoteRequestDto.urn] = emoteRequestDto.individualData[0].id; |
| | 93 | | } |
| | 94 | |
|
| 0 | 95 | | IReadOnlyList<WearableItem> emotes = await FetchEmotes(emoteUrns, urnToAmountMap); |
| | 96 | |
|
| 0 | 97 | | tempList.Dispose(); |
| | 98 | |
|
| 0 | 99 | | OnOwnedEmotesReceived?.Invoke(emotes, userId, idToExtendedUrn); |
| 0 | 100 | | } |
| | 101 | |
|
| | 102 | | // This recursiveness is horrible, we should add proper pagination |
| | 103 | | private async UniTask<bool> FullListEmoteFetch(string userId, string url, List<OwnedEmotesRequestDto.EmoteReques |
| | 104 | | { |
| 0 | 105 | | var requestedCount = 0; |
| 0 | 106 | | var totalEmotes = 99999; |
| 0 | 107 | | var pageNum = 1; |
| | 108 | |
|
| 0 | 109 | | while (requestedCount < totalEmotes) |
| | 110 | | { |
| 0 | 111 | | (string name, string value)[] queryParams = new List<(string name, string value)> |
| | 112 | | { |
| | 113 | | ("pageNum", pageNum.ToString()), |
| | 114 | | }.ToArray(); |
| | 115 | |
|
| 0 | 116 | | (OwnedEmotesRequestDto response, bool success) result = await lambdasService.GetFromSpecificUrl<OwnedEmo |
| | 117 | | cancellationToken: cts.Token, urlEncodedParams: queryParams); |
| | 118 | |
|
| 0 | 119 | | if (!result.success) throw new Exception($"Fetching owned wearables failed! {url}\nAddress: {userId}"); |
| | 120 | |
|
| 0 | 121 | | if (result.response.elements.Length <= 0 && requestedCount <= 0) |
| | 122 | | { |
| 0 | 123 | | OnOwnedEmotesReceived?.Invoke(new List<WearableItem>(), userId, new Dictionary<string, string>()); |
| 0 | 124 | | return false; |
| | 125 | | } |
| | 126 | |
|
| 0 | 127 | | requestedCount += result.response.elements.Length; |
| 0 | 128 | | totalEmotes = result.response.totalAmount; |
| 0 | 129 | | pageNum++; |
| 0 | 130 | | requestedEmotes.AddRange(result.response.elements); |
| | 131 | | } |
| | 132 | |
|
| 0 | 133 | | return true; |
| 0 | 134 | | } |
| | 135 | |
|
| | 136 | | public void RequestEmote(string emoteId) |
| | 137 | | { |
| 0 | 138 | | RequestWearableBatchAsync(emoteId).Forget(); |
| 0 | 139 | | } |
| | 140 | |
|
| | 141 | | private async UniTask RequestWearableBatchAsync(string id) |
| | 142 | | { |
| 0 | 143 | | pendingRequests.Add(id); |
| 0 | 144 | | lastRequestSource ??= new UniTaskCompletionSource<IReadOnlyList<WearableItem>>(); |
| 0 | 145 | | UniTaskCompletionSource<IReadOnlyList<WearableItem>> sourceToAwait = lastRequestSource; |
| | 146 | |
|
| | 147 | | // we wait for the latest update possible so we buffer all requests into one |
| 0 | 148 | | await UniTask.Yield(PlayerLoopTiming.PostLateUpdate, cts.Token); |
| | 149 | |
|
| | 150 | | IReadOnlyList<WearableItem> result; |
| | 151 | |
|
| 0 | 152 | | if (pendingRequests.Count > 0) |
| | 153 | | { |
| 0 | 154 | | lastRequestSource = null; |
| | 155 | |
|
| 0 | 156 | | List<string> tempList = ListPool<string>.Get(); |
| 0 | 157 | | tempList.AddRange(pendingRequests); |
| 0 | 158 | | pendingRequests.Clear(); |
| | 159 | |
|
| 0 | 160 | | result = await FetchEmotes(tempList); |
| 0 | 161 | | ListPool<string>.Release(tempList); |
| 0 | 162 | | sourceToAwait.TrySetResult(result); |
| 0 | 163 | | OnEmotesReceived?.Invoke(result); |
| 0 | 164 | | } |
| | 165 | | else |
| 0 | 166 | | await sourceToAwait.Task; |
| 0 | 167 | | } |
| | 168 | |
|
| | 169 | | private async UniTask<IReadOnlyList<WearableItem>> FetchEmotes( |
| | 170 | | IReadOnlyCollection<string> ids, |
| | 171 | | Dictionary<string, int> urnToAmountMap = null) |
| | 172 | | { |
| | 173 | | // the copy of the list is intentional |
| 0 | 174 | | var request = new LambdasEmotesCatalogService.WearableRequest { pointers = new List<string>(ids) }; |
| 0 | 175 | | var url = $"{catalyst.contentUrl}entities/active"; |
| | 176 | |
|
| 0 | 177 | | (EmoteEntityDto[] response, bool success) response = await lambdasService.PostFromSpecificUrl<EmoteEntityDto |
| | 178 | | url, url, request, cancellationToken: cts.Token); |
| | 179 | |
|
| 0 | 180 | | if (!response.success) throw new Exception($"Fetching wearables failed! {url}\n{string.Join("\n", request.po |
| | 181 | |
|
| 0 | 182 | | HashSet<string> receivedIds = HashSetPool<string>.Get(); |
| | 183 | |
|
| 0 | 184 | | IEnumerable<WearableItem> wearables = response.response.Select(dto => |
| | 185 | | { |
| 0 | 186 | | var contentUrl = $"{catalyst.contentUrl}contents/"; |
| 0 | 187 | | var wearableItem = dto.ToWearableItem(contentUrl); |
| | 188 | |
|
| 0 | 189 | | if (urnToAmountMap != null && urnToAmountMap.TryGetValue(dto.metadata.id, out int amount)) |
| 0 | 190 | | wearableItem.amount = amount; |
| | 191 | |
|
| 0 | 192 | | wearableItem.baseUrl = contentUrl; |
| 0 | 193 | | wearableItem.baseUrlBundles = assetBundlesUrl; |
| 0 | 194 | | return wearableItem; |
| | 195 | | }); |
| | 196 | |
|
| 0 | 197 | | foreach (WearableItem wearableItem in wearables) |
| 0 | 198 | | receivedIds.Add(wearableItem.id); |
| | 199 | |
|
| 0 | 200 | | foreach (string id in ids) |
| 0 | 201 | | if (!receivedIds.Contains(id)) |
| 0 | 202 | | OnEmoteRejected?.Invoke(id, "Empty response from content server"); |
| | 203 | |
|
| 0 | 204 | | HashSetPool<string>.Release(receivedIds); |
| 0 | 205 | | return wearables.ToList(); |
| 0 | 206 | | } |
| | 207 | | } |
| | 208 | | } |