| | 1 | | using Cysharp.Threading.Tasks; |
| | 2 | | using DCL; |
| | 3 | | using DCL.Tasks; |
| | 4 | | using DCLServices.Lambdas; |
| | 5 | | using MainScripts.DCL.Helpers.Utils; |
| | 6 | | using System; |
| | 7 | | using System.Collections.Generic; |
| | 8 | | using System.Linq; |
| | 9 | | using System.Threading; |
| | 10 | | using UnityEngine; |
| | 11 | | using UnityEngine.Pool; |
| | 12 | | using static DCLServices.WearablesCatalogService.WearableWithEntityResponseDto.ElementDto; |
| | 13 | |
|
| | 14 | | namespace DCLServices.WearablesCatalogService |
| | 15 | | { |
| | 16 | | /// <summary> |
| | 17 | | /// This service implements a direct way of getting wearables sending the requests directly to lambdas. |
| | 18 | | /// </summary> |
| | 19 | | public class LambdasWearablesCatalogService : IWearablesCatalogService, ILambdaServiceConsumer<WearableWithDefinitio |
| | 20 | | { |
| | 21 | | [Serializable] |
| | 22 | | public class WearableRequest |
| | 23 | | { |
| | 24 | | public List<string> pointers; |
| | 25 | | } |
| | 26 | |
|
| | 27 | | [Serializable] |
| | 28 | | public class WearableCollectionResponse |
| | 29 | | { |
| | 30 | | public int total; |
| | 31 | | public EntityDto[] entities; |
| | 32 | |
|
| 0 | 33 | | public WearableCollectionResponse() { } |
| | 34 | |
|
| 14 | 35 | | public WearableCollectionResponse(EntityDto[] entities) |
| | 36 | | { |
| 14 | 37 | | this.entities = entities; |
| 14 | 38 | | } |
| | 39 | | } |
| | 40 | |
|
| 446 | 41 | | public BaseDictionary<string, WearableItem> WearablesCatalog { get; } |
| | 42 | |
|
| | 43 | | private const string PAGINATED_WEARABLES_END_POINT = "users/"; |
| | 44 | | private const string NON_PAGINATED_WEARABLES_END_POINT = "collections/wearables/"; |
| | 45 | | private const string THIRD_PARTY_COLLECTIONS_FETCH_URL = "third-party-integrations"; |
| | 46 | | private const int REQUESTS_TIME_OUT_SECONDS = 45; |
| | 47 | | private const int MAX_WEARABLES_PER_REQUEST = 200; |
| | 48 | |
|
| | 49 | | private readonly ILambdasService lambdasService; |
| 129 | 50 | | private readonly Dictionary<string, int> wearablesInUseCounters = new (new Dictionary<string, int>(), StringIgno |
| 129 | 51 | | private readonly Dictionary<(string userId, int pageSize), LambdaResponsePagePointer<WearableWithDefinitionRespo |
| 129 | 52 | | private readonly Dictionary<(string userId, string collectionId, int pageSize), LambdaResponsePagePointer<Wearab |
| 129 | 53 | | private readonly List<string> pendingWearablesToRequest = new (); |
| | 54 | | private readonly BaseVariable<FeatureFlag> featureFlags; |
| | 55 | | private readonly DataStore dataStore; |
| | 56 | | private readonly KernelConfig kernelConfig; |
| | 57 | | private readonly ICatalyst catalyst; |
| | 58 | |
|
| 16 | 59 | | private string assetBundlesUrl => featureFlags.Get().IsFeatureEnabled("ab-new-cdn") ? "https://ab-cdn.decentrala |
| | 60 | |
|
| | 61 | | private CancellationTokenSource serviceCts; |
| | 62 | | private UniTaskCompletionSource<IReadOnlyList<WearableItem>> lastRequestSource; |
| | 63 | |
|
| 129 | 64 | | public LambdasWearablesCatalogService(BaseDictionary<string, WearableItem> wearablesCatalog, |
| | 65 | | ILambdasService lambdasService, |
| | 66 | | IServiceProviders serviceProviders, |
| | 67 | | BaseVariable<FeatureFlag> featureFlags, |
| | 68 | | DataStore dataStore, |
| | 69 | | KernelConfig kernelConfig) |
| | 70 | | { |
| 129 | 71 | | this.featureFlags = featureFlags; |
| 129 | 72 | | this.dataStore = dataStore; |
| 129 | 73 | | this.kernelConfig = kernelConfig; |
| 129 | 74 | | this.lambdasService = lambdasService; |
| 129 | 75 | | WearablesCatalog = wearablesCatalog; |
| 129 | 76 | | catalyst = serviceProviders.catalyst; |
| 129 | 77 | | } |
| | 78 | |
|
| | 79 | | public void Initialize() |
| | 80 | | { |
| 54 | 81 | | serviceCts = serviceCts.SafeRestart(); |
| 54 | 82 | | } |
| | 83 | |
|
| | 84 | | public void Dispose() |
| | 85 | | { |
| 14 | 86 | | serviceCts.SafeCancelAndDispose(); |
| 14 | 87 | | serviceCts = null; |
| 14 | 88 | | Clear(); |
| 14 | 89 | | } |
| | 90 | |
|
| | 91 | | public async UniTask<WearableCollectionsAPIData.Collection[]> GetThirdPartyCollectionsAsync(CancellationToken ca |
| | 92 | | { |
| 0 | 93 | | (WearableCollectionsAPIData response, bool success) = await lambdasService.Get<WearableCollectionsAPIData>(T |
| | 94 | | THIRD_PARTY_COLLECTIONS_FETCH_URL, cancellationToken: cancellationToken); |
| | 95 | |
|
| 0 | 96 | | if (!success) |
| 0 | 97 | | throw new Exception("Request error! third party collections couldn't be fetched!"); |
| | 98 | |
|
| 0 | 99 | | return response.data; |
| 0 | 100 | | } |
| | 101 | |
|
| | 102 | | public async UniTask<(IReadOnlyList<WearableItem> wearables, int totalAmount)> RequestOwnedWearablesAsync( |
| | 103 | | string userId, int pageNumber, int pageSize, CancellationToken cancellationToken, string category = null, |
| | 104 | | NftRarity rarity = NftRarity.None, |
| | 105 | | NftCollectionType collectionTypeMask = NftCollectionType.All, |
| | 106 | | ICollection<string> thirdPartyCollectionIds = null, |
| | 107 | | string name = null, (NftOrderByOperation type, bool directionAscendent)? orderBy = null) |
| | 108 | | { |
| 4 | 109 | | var queryParams = new List<(string name, string value)> |
| | 110 | | { |
| | 111 | | ("pageNum", pageNumber.ToString()), |
| | 112 | | ("pageSize", pageSize.ToString()), |
| | 113 | | ("includeEntities", "true"), |
| | 114 | | }; |
| | 115 | |
|
| 4 | 116 | | if (rarity != NftRarity.None) |
| 1 | 117 | | queryParams.Add(("rarity", rarity.ToString().ToLower())); |
| | 118 | |
|
| 4 | 119 | | if (!string.IsNullOrEmpty(category)) |
| 1 | 120 | | queryParams.Add(("category", category)); |
| | 121 | |
|
| 4 | 122 | | if (!string.IsNullOrEmpty(name)) |
| 1 | 123 | | queryParams.Add(("name", name)); |
| | 124 | |
|
| 4 | 125 | | if (orderBy != null) |
| | 126 | | { |
| 1 | 127 | | queryParams.Add(("orderBy", orderBy.Value.type.ToString().ToLower())); |
| 1 | 128 | | queryParams.Add(("direction", orderBy.Value.directionAscendent ? "ASC" : "DESC")); |
| | 129 | | } |
| | 130 | |
|
| 4 | 131 | | if ((collectionTypeMask & NftCollectionType.Base) != 0) |
| 2 | 132 | | queryParams.Add(("collectionType", "base-wearable")); |
| | 133 | |
|
| 4 | 134 | | if ((collectionTypeMask & NftCollectionType.OnChain) != 0) |
| 2 | 135 | | queryParams.Add(("collectionType", "on-chain")); |
| | 136 | |
|
| 4 | 137 | | if ((collectionTypeMask & NftCollectionType.ThirdParty) != 0) |
| 2 | 138 | | queryParams.Add(("collectionType", "third-party")); |
| | 139 | |
|
| 4 | 140 | | if (thirdPartyCollectionIds != null) |
| 4 | 141 | | foreach (string collectionId in thirdPartyCollectionIds) |
| 1 | 142 | | queryParams.Add(("thirdPartyCollectionId", collectionId)); |
| | 143 | |
|
| | 144 | | string explorerUrl; |
| | 145 | | string contentUrl; |
| | 146 | |
|
| 4 | 147 | | if (IsLocalPreview()) |
| | 148 | | { |
| 0 | 149 | | explorerUrl = "https://peer.decentraland.org/explorer/"; |
| 0 | 150 | | contentUrl = "https://peer.decentraland.org/content/contents/"; |
| | 151 | | } |
| | 152 | | else |
| | 153 | | { |
| 4 | 154 | | string lambdasUrl = await catalyst.GetLambdaUrl(cancellationToken); |
| 4 | 155 | | explorerUrl = lambdasUrl.Replace("/lambdas", "/explorer"); |
| 4 | 156 | | contentUrl = $"{catalyst.contentUrl}/contents/"; |
| | 157 | | } |
| | 158 | |
|
| 4 | 159 | | (WearableWithEntityResponseDto response, bool success) = await lambdasService.GetFromSpecificUrl<WearableWit |
| | 160 | | $"{explorerUrl}/:userId/wearables", |
| | 161 | | $"{explorerUrl}/{userId}/wearables", |
| | 162 | | cancellationToken: cancellationToken, |
| | 163 | | urlEncodedParams: queryParams.ToArray()); |
| | 164 | |
|
| 4 | 165 | | if (!success) |
| 0 | 166 | | throw new Exception($"The request of wearables for '{userId}' failed!"); |
| | 167 | |
|
| 4 | 168 | | List<WearableItem> wearables = ValidateWearables(response.elements, |
| | 169 | | contentUrl, |
| | 170 | | assetBundlesUrl); |
| | 171 | |
|
| 4 | 172 | | AddWearablesToCatalog(wearables); |
| | 173 | |
|
| 4 | 174 | | return (wearables, response.TotalAmount); |
| 4 | 175 | | } |
| | 176 | |
|
| | 177 | | public async UniTask<(IReadOnlyList<WearableItem> wearables, int totalAmount)> RequestOwnedWearablesAsync(string |
| | 178 | | { |
| 1 | 179 | | var createNewPointer = false; |
| | 180 | |
|
| 2 | 181 | | if (!ownerWearablesPagePointers.TryGetValue((userId, pageSize), out var pagePointer)) { createNewPointer = t |
| 0 | 182 | | else if (cleanCachedPages) |
| | 183 | | { |
| 0 | 184 | | pagePointer.Dispose(); |
| 0 | 185 | | ownerWearablesPagePointers.Remove((userId, pageSize)); |
| 0 | 186 | | createNewPointer = true; |
| | 187 | | } |
| | 188 | |
|
| 1 | 189 | | if (createNewPointer) |
| | 190 | | { |
| 1 | 191 | | ownerWearablesPagePointers[(userId, pageSize)] = pagePointer = new LambdaResponsePagePointer<WearableWit |
| | 192 | | $"{PAGINATED_WEARABLES_END_POINT}{userId}/wearables", |
| | 193 | | pageSize, ct, this); |
| | 194 | | } |
| | 195 | |
|
| 1 | 196 | | var pageResponse = await pagePointer.GetPageAsync(pageNumber, ct); |
| | 197 | |
|
| 1 | 198 | | if (!pageResponse.success) |
| 0 | 199 | | throw new Exception($"The request of the owned wearables for '{userId}' failed!"); |
| | 200 | |
|
| 3 | 201 | | var wearables = pageResponse.response.elements.Select(x => x.definition).ToList(); |
| 1 | 202 | | MapLambdasDataIntoWearableItem(wearables); |
| 1 | 203 | | AddWearablesToCatalog(wearables); |
| | 204 | |
|
| 1 | 205 | | return (wearables, pageResponse.response.TotalAmount); |
| 1 | 206 | | } |
| | 207 | |
|
| | 208 | | public async UniTask RequestBaseWearablesAsync(CancellationToken ct) |
| | 209 | | { |
| 1 | 210 | | var url = $"{catalyst.contentUrl}entities/active/collections/{IWearablesCatalogService.BASE_WEARABLES_COLLEC |
| | 211 | |
|
| 1 | 212 | | var request = await lambdasService.GetFromSpecificUrl<WearableCollectionResponse>(url, url, cancellationToke |
| | 213 | |
|
| 1 | 214 | | if (!request.success) |
| 0 | 215 | | throw new Exception("The request of the base wearables failed!"); |
| | 216 | |
|
| 1 | 217 | | var poolList = PoolUtils.RentList<WearableItem>(); |
| 1 | 218 | | IList<WearableItem> wearableItems = poolList.GetList(); |
| | 219 | |
|
| 6 | 220 | | foreach (EntityDto entityDto in request.response.entities) |
| 2 | 221 | | wearableItems.Add(entityDto.ToWearableItem(catalyst.contentUrl, assetBundlesUrl, 1)); |
| | 222 | |
|
| 1 | 223 | | MapLambdasDataIntoWearableItem(wearableItems); |
| 1 | 224 | | AddWearablesToCatalog(wearableItems); |
| | 225 | |
|
| 1 | 226 | | poolList.Dispose(); |
| 1 | 227 | | } |
| | 228 | |
|
| | 229 | | public async UniTask<(IReadOnlyList<WearableItem> wearables, int totalAmount)> RequestThirdPartyWearablesByColle |
| | 230 | | CancellationToken ct) |
| | 231 | | { |
| 1 | 232 | | var createNewPointer = false; |
| | 233 | |
|
| 2 | 234 | | if (!thirdPartyCollectionPagePointers.TryGetValue((userId, collectionId, pageSize), out var pagePointer)) { |
| 0 | 235 | | else if (cleanCachedPages) |
| | 236 | | { |
| 0 | 237 | | pagePointer.Dispose(); |
| 0 | 238 | | thirdPartyCollectionPagePointers.Remove((userId, collectionId, pageSize)); |
| 0 | 239 | | createNewPointer = true; |
| | 240 | | } |
| | 241 | |
|
| 1 | 242 | | if (createNewPointer) |
| | 243 | | { |
| 1 | 244 | | thirdPartyCollectionPagePointers[(userId, collectionId, pageSize)] = pagePointer = new LambdaResponsePag |
| | 245 | | $"{PAGINATED_WEARABLES_END_POINT}{userId}/third-party-wearables/{collectionId}", |
| | 246 | | pageSize, ct, this); |
| | 247 | | } |
| | 248 | |
|
| 1 | 249 | | var pageResponse = await pagePointer.GetPageAsync(pageNumber, ct); |
| | 250 | |
|
| 1 | 251 | | if (!pageResponse.success) |
| 0 | 252 | | throw new Exception($"The request of the '{collectionId}' third party wearables collection of '{userId}' |
| | 253 | |
|
| 3 | 254 | | var wearables = pageResponse.response.elements.Select(x => x.definition).ToList(); |
| 1 | 255 | | MapLambdasDataIntoWearableItem(wearables); |
| 1 | 256 | | AddWearablesToCatalog(wearables); |
| | 257 | |
|
| 1 | 258 | | return (wearables, pageResponse.response.TotalAmount); |
| 1 | 259 | | } |
| | 260 | |
|
| | 261 | | public async UniTask<WearableItem> RequestWearableAsync(string wearableId, CancellationToken ct) |
| | 262 | | { |
| 3 | 263 | | if (WearablesCatalog.TryGetValue(wearableId, out WearableItem wearable)) |
| | 264 | | { |
| 1 | 265 | | if (wearablesInUseCounters.ContainsKey(wearableId)) |
| 1 | 266 | | wearablesInUseCounters[wearableId]++; |
| | 267 | |
|
| 1 | 268 | | return wearable; |
| | 269 | | } |
| | 270 | |
|
| 2 | 271 | | ct.ThrowIfCancellationRequested(); |
| | 272 | |
|
| | 273 | | try |
| | 274 | | { |
| | 275 | | // All the requests happened during the same frames interval are sent together |
| 6 | 276 | | return await SyncWearablesRequestsAsync(wearableId, ct); |
| | 277 | | } |
| 0 | 278 | | catch (OperationCanceledException) { return null; } |
| 3 | 279 | | } |
| | 280 | |
|
| | 281 | | public async UniTask<WearableItem> RequestWearableFromBuilderAsync(string wearableId, CancellationToken ct) |
| | 282 | | { |
| 0 | 283 | | if (WearablesCatalog.TryGetValue(wearableId, out WearableItem wearable)) |
| | 284 | | { |
| 0 | 285 | | if (wearablesInUseCounters.ContainsKey(wearableId)) |
| 0 | 286 | | wearablesInUseCounters[wearableId]++; |
| | 287 | |
|
| 0 | 288 | | return wearable; |
| | 289 | | } |
| | 290 | |
|
| 0 | 291 | | string domain = GetBuilderDomainUrl(); |
| 0 | 292 | | var url = $"{domain}/items/{wearableId}"; |
| | 293 | |
|
| 0 | 294 | | (WearableItemResponseFromBuilder response, bool success) = await lambdasService.GetFromSpecificUrl<WearableI |
| | 295 | | domain, url, |
| | 296 | | isSigned: true, |
| | 297 | | cancellationToken: ct); |
| | 298 | |
|
| 0 | 299 | | if (!success) |
| 0 | 300 | | throw new Exception($"The request of wearables from builder '{wearableId}' failed!"); |
| | 301 | |
|
| 0 | 302 | | List<WearableItem> ws = new List<WearableItem> |
| | 303 | | { |
| | 304 | | response.data.ToWearableItem( |
| | 305 | | $"{domain}/storage/contents/", |
| | 306 | | assetBundlesUrl), |
| | 307 | | }; |
| | 308 | |
|
| 0 | 309 | | if (ws[0].IsEmote()) return null; |
| | 310 | |
|
| 0 | 311 | | AddWearablesToCatalog(ws); |
| | 312 | |
|
| 0 | 313 | | return ws[0]; |
| 0 | 314 | | } |
| | 315 | |
|
| | 316 | | public async UniTask<IReadOnlyList<WearableItem>> RequestWearableCollection(IEnumerable<string> collectionIds, |
| | 317 | | CancellationToken cancellationToken, List<WearableItem> collectionBuffer = null) |
| | 318 | | { |
| 0 | 319 | | List<WearableItem> wearables = collectionBuffer ?? new List<WearableItem>(); |
| 0 | 320 | | var templateURL = $"{catalyst.contentUrl}entities/active/collections/:collectionId"; |
| | 321 | |
|
| 0 | 322 | | foreach (string collectionId in collectionIds) |
| | 323 | | { |
| 0 | 324 | | string url = templateURL.Replace(":collectionId", collectionId); |
| | 325 | |
|
| 0 | 326 | | (WearableCollectionResponse response, bool success) = await lambdasService.GetFromSpecificUrl<WearableCo |
| | 327 | | templateURL, url, |
| | 328 | | cancellationToken: cancellationToken); |
| | 329 | |
|
| 0 | 330 | | if (!success) |
| 0 | 331 | | throw new Exception($"The request for collection of wearables '{collectionId}' failed!"); |
| | 332 | |
|
| 0 | 333 | | var poolList = PoolUtils.RentList<WearableItem>(); |
| 0 | 334 | | IList<WearableItem> wearableItems = poolList.GetList(); |
| | 335 | |
|
| 0 | 336 | | foreach (EntityDto entityDto in response.entities) |
| 0 | 337 | | wearableItems.Add(entityDto.ToWearableItem(catalyst.contentUrl, assetBundlesUrl, 1)); |
| | 338 | |
|
| 0 | 339 | | MapLambdasDataIntoWearableItem(wearableItems); |
| 0 | 340 | | AddWearablesToCatalog(wearableItems); |
| | 341 | |
|
| 0 | 342 | | wearables.AddRange(wearableItems); |
| 0 | 343 | | poolList.Dispose(); |
| 0 | 344 | | } |
| | 345 | |
|
| 0 | 346 | | return wearables; |
| 0 | 347 | | } |
| | 348 | |
|
| | 349 | | public async UniTask<(IReadOnlyList<WearableItem> wearables, int totalAmount)> RequestWearableCollectionInBuilde |
| | 350 | | CancellationToken cancellationToken, List<WearableItem> collectionBuffer = null, string nameFilter = null, |
| | 351 | | int pageNumber = 1, int pageSize = 5000) |
| | 352 | | { |
| 0 | 353 | | string domain = GetBuilderDomainUrl(); |
| 0 | 354 | | var wearables = collectionBuffer ?? new List<WearableItem>(); |
| | 355 | |
|
| 0 | 356 | | var queryParams = new[] |
| | 357 | | { |
| | 358 | | ("page", pageNumber.ToString()), |
| | 359 | | ("limit", pageSize.ToString()), |
| | 360 | | ("name", nameFilter), |
| | 361 | | }; |
| | 362 | |
|
| 0 | 363 | | var totalAmount = 0; |
| | 364 | |
|
| 0 | 365 | | foreach (string collectionId in collectionIds) |
| | 366 | | { |
| 0 | 367 | | var url = $"{domain}/collections/{collectionId}/items/"; |
| 0 | 368 | | var templateUrl = $"{domain}/collections/:collectionId/items/"; |
| | 369 | |
|
| 0 | 370 | | (WearableCollectionResponseFromBuilder response, bool success) = await lambdasService.GetFromSpecificUrl |
| | 371 | | templateUrl, url, |
| | 372 | | isSigned: true, |
| | 373 | | // urlEncodedParams: queryParams.Select(pair => (pair.Key, pair.Value)).ToArray(), |
| | 374 | | urlEncodedParams: queryParams, |
| | 375 | | cancellationToken: cancellationToken); |
| | 376 | |
|
| 0 | 377 | | if (!success) |
| 0 | 378 | | throw new Exception($"The request for collection of wearables from builder '{collectionId}' failed!" |
| | 379 | |
|
| 0 | 380 | | List<WearableItem> ws = response.data.results |
| 0 | 381 | | .Select(bw => bw.ToWearableItem( |
| | 382 | | $"{domain}/storage/contents/", |
| | 383 | | assetBundlesUrl)) |
| 0 | 384 | | .Where(bw => !bw.IsEmote()) |
| | 385 | | .ToList(); |
| | 386 | |
|
| 0 | 387 | | AddWearablesToCatalog(ws); |
| | 388 | |
|
| 0 | 389 | | wearables.AddRange(ws); |
| 0 | 390 | | totalAmount = response.data.total; |
| 0 | 391 | | } |
| | 392 | |
|
| 0 | 393 | | return (wearables, totalAmount); |
| 0 | 394 | | } |
| | 395 | |
|
| | 396 | | public void AddWearablesToCatalog(IEnumerable<WearableItem> wearableItems) |
| | 397 | | { |
| 62 | 398 | | foreach (WearableItem wearableItem in wearableItems) |
| | 399 | | { |
| 17 | 400 | | if (WearablesCatalog.ContainsKey(wearableItem.id)) |
| | 401 | | continue; |
| | 402 | |
|
| 17 | 403 | | wearableItem.SanitizeHidesLists(); |
| 17 | 404 | | WearablesCatalog.Add(wearableItem.id, wearableItem); |
| | 405 | |
|
| 17 | 406 | | if (!wearablesInUseCounters.ContainsKey(wearableItem.id)) |
| 17 | 407 | | wearablesInUseCounters.Add(wearableItem.id, 1); |
| | 408 | | } |
| 14 | 409 | | } |
| | 410 | |
|
| | 411 | | public void RemoveWearablesFromCatalog(IEnumerable<string> wearableIds) |
| | 412 | | { |
| 6 | 413 | | foreach (string wearableId in wearableIds) |
| 2 | 414 | | RemoveWearableFromCatalog(wearableId); |
| 1 | 415 | | } |
| | 416 | |
|
| | 417 | | public void RemoveWearableFromCatalog(string wearableId) |
| | 418 | | { |
| 2 | 419 | | WearablesCatalog.Remove(wearableId); |
| 2 | 420 | | wearablesInUseCounters.Remove(wearableId); |
| 2 | 421 | | } |
| | 422 | |
|
| | 423 | | public void RemoveWearablesInUse(IEnumerable<string> wearablesInUseToRemove) |
| | 424 | | { |
| 6 | 425 | | foreach (string wearableToRemove in wearablesInUseToRemove) |
| | 426 | | { |
| 2 | 427 | | if (!wearablesInUseCounters.ContainsKey(wearableToRemove)) |
| | 428 | | continue; |
| | 429 | |
|
| 1 | 430 | | wearablesInUseCounters[wearableToRemove]--; |
| | 431 | |
|
| 1 | 432 | | if (wearablesInUseCounters[wearableToRemove] > 0) |
| | 433 | | continue; |
| | 434 | |
|
| 1 | 435 | | WearablesCatalog.Remove(wearableToRemove); |
| 1 | 436 | | wearablesInUseCounters.Remove(wearableToRemove); |
| | 437 | | } |
| 1 | 438 | | } |
| | 439 | |
|
| | 440 | | public void AddEmbeddedWearablesToCatalog(IEnumerable<WearableItem> wearables) |
| | 441 | | { |
| 774 | 442 | | foreach (WearableItem wearableItem in wearables) |
| | 443 | | { |
| 375 | 444 | | WearablesCatalog[wearableItem.id] = wearableItem; |
| | 445 | |
|
| 375 | 446 | | if (wearablesInUseCounters.ContainsKey(wearableItem.id)) |
| 0 | 447 | | wearablesInUseCounters[wearableItem.id] = int.MaxValue; //A high value to ensure they are not remove |
| | 448 | | } |
| 12 | 449 | | } |
| | 450 | |
|
| | 451 | | public void Clear() |
| | 452 | | { |
| 14 | 453 | | WearablesCatalog.Clear(); |
| 14 | 454 | | wearablesInUseCounters.Clear(); |
| 14 | 455 | | pendingWearablesToRequest.Clear(); |
| 14 | 456 | | ownerWearablesPagePointers.Clear(); |
| 14 | 457 | | thirdPartyCollectionPagePointers.Clear(); |
| 14 | 458 | | } |
| | 459 | |
|
| | 460 | | public bool IsValidWearable(string wearableId) |
| | 461 | | { |
| 4 | 462 | | if (!WearablesCatalog.TryGetValue(wearableId, out var wearable)) |
| 2 | 463 | | return false; |
| | 464 | |
|
| 2 | 465 | | return wearable != null; |
| | 466 | | } |
| | 467 | |
|
| | 468 | | UniTask<(WearableWithDefinitionResponse response, bool success)> ILambdaServiceConsumer<WearableWithDefinitionRe |
| | 469 | | (string endPoint, int pageSize, int pageNumber, Dictionary<string, string> additionalData, CancellationToken |
| 2 | 470 | | lambdasService.Get<WearableWithDefinitionResponse>( |
| | 471 | | PAGINATED_WEARABLES_END_POINT, |
| | 472 | | endPoint, |
| | 473 | | REQUESTS_TIME_OUT_SECONDS, |
| | 474 | | ILambdasService.DEFAULT_ATTEMPTS_NUMBER, |
| | 475 | | cancellationToken, |
| | 476 | | LambdaPaginatedResponseHelper.GetPageSizeParam(pageSize), |
| | 477 | | LambdaPaginatedResponseHelper.GetPageNumParam(pageNumber), |
| | 478 | | ("includeDefinitions", "true")); |
| | 479 | |
|
| | 480 | | private async UniTask<WearableItem> SyncWearablesRequestsAsync(string newWearableId, CancellationToken ct) |
| | 481 | | { |
| 2 | 482 | | pendingWearablesToRequest.Add(newWearableId); |
| 2 | 483 | | lastRequestSource ??= new UniTaskCompletionSource<IReadOnlyList<WearableItem>>(); |
| 2 | 484 | | var sourceToAwait = lastRequestSource; |
| | 485 | |
|
| 6 | 486 | | await UniTask.Yield(PlayerLoopTiming.PostLateUpdate, cancellationToken: ct); |
| | 487 | |
|
| 2 | 488 | | List<WearableItem> result = new List<WearableItem>(); |
| | 489 | |
|
| 2 | 490 | | if (pendingWearablesToRequest.Count > 0) |
| | 491 | | { |
| 2 | 492 | | lastRequestSource = null; |
| | 493 | |
|
| 2 | 494 | | using var wearableIdsPool = PoolUtils.RentList<string>(); |
| 2 | 495 | | var wearableIds = wearableIdsPool.GetList(); |
| 2 | 496 | | wearableIds.AddRange(pendingWearablesToRequest); |
| 2 | 497 | | pendingWearablesToRequest.Clear(); |
| | 498 | |
|
| | 499 | | // When the number of wearables to request is greater than MAX_WEARABLES_PER_REQUEST, we split the reque |
| | 500 | | // In this way we avoid to send a very long url string that would fail due to the web request size limit |
| 2 | 501 | | int numberOfPartialRequests = (wearableIds.Count + MAX_WEARABLES_PER_REQUEST - 1) / MAX_WEARABLES_PER_RE |
| 2 | 502 | | var awaitingPartialTasksPool = PoolUtils.RentList<(UniTask<(EntityDto[] response, bool success)> task, I |
| 2 | 503 | | var awaitingPartialTasks = awaitingPartialTasksPool.GetList(); |
| | 504 | |
|
| 8 | 505 | | for (var i = 0; i < numberOfPartialRequests; i++) |
| | 506 | | { |
| 2 | 507 | | int numberOfWearablesToRequest = wearableIds.Count < MAX_WEARABLES_PER_REQUEST |
| | 508 | | ? wearableIds.Count |
| | 509 | | : MAX_WEARABLES_PER_REQUEST; |
| | 510 | |
|
| 2 | 511 | | var wearablesToRequest = new List<string>(); |
| 2 | 512 | | int count = Math.Min(wearableIds.Count, numberOfWearablesToRequest); |
| | 513 | |
|
| 8 | 514 | | for (int x = 0; x < count; x++) |
| | 515 | | { |
| 2 | 516 | | var urnAndTokenId = ExtendedUrnParser.GetShortenedUrn(wearableIds[x]); |
| 2 | 517 | | wearablesToRequest.Add(urnAndTokenId); |
| | 518 | | } |
| | 519 | |
|
| 2 | 520 | | var request = new WearableRequest { pointers = wearablesToRequest }; |
| 2 | 521 | | var url = $"{catalyst.contentUrl}entities/active"; |
| | 522 | |
|
| 2 | 523 | | var partialTask = lambdasService.PostFromSpecificUrl<EntityDto[], WearableRequest>(url, url, request |
| | 524 | |
|
| 2 | 525 | | wearableIds.RemoveRange(0, numberOfWearablesToRequest); |
| 2 | 526 | | awaitingPartialTasks.Add((partialTask, wearablesToRequest)); |
| | 527 | | } |
| | 528 | |
|
| 2 | 529 | | var servicePartialResponsesPool = PoolUtils.RentList<((EntityDto[] response, bool success) taskResponse, |
| 2 | 530 | | var servicePartialResponses = servicePartialResponsesPool.GetList(); |
| | 531 | |
|
| | 532 | | try |
| | 533 | | { |
| 8 | 534 | | foreach (var partialTask in awaitingPartialTasks) |
| 2 | 535 | | servicePartialResponses.Add((await partialTask.task, partialTask.wearablesRequested)); |
| 2 | 536 | | } |
| 0 | 537 | | catch (Exception e) |
| | 538 | | { |
| 0 | 539 | | sourceToAwait.TrySetException(e); |
| 0 | 540 | | throw; |
| | 541 | | } |
| | 542 | |
|
| 8 | 543 | | foreach (var partialResponse in servicePartialResponses) |
| | 544 | | { |
| 2 | 545 | | if (!partialResponse.taskResponse.success) |
| | 546 | | { |
| 0 | 547 | | Exception e = new Exception($"The request of the wearables ('{string.Join(", ", partialResponse. |
| 0 | 548 | | sourceToAwait.TrySetException(e); |
| 0 | 549 | | throw e; |
| | 550 | | } |
| | 551 | |
|
| 2 | 552 | | var response = partialResponse.taskResponse.response; |
| | 553 | |
|
| 2 | 554 | | string contentBaseUrl = $"{catalyst.contentUrl}contents/"; |
| | 555 | |
|
| 4 | 556 | | var wearables = response.Select(dto => dto.ToWearableItem(contentBaseUrl, assetBundlesUrl, 1)).ToLis |
| | 557 | |
|
| 2 | 558 | | MapLambdasDataIntoWearableItem(wearables); |
| 2 | 559 | | AddWearablesToCatalog(wearables); |
| 2 | 560 | | result.AddRange(wearables); |
| | 561 | | } |
| | 562 | |
|
| 2 | 563 | | sourceToAwait.TrySetResult(result); |
| 2 | 564 | | } |
| | 565 | | else |
| 0 | 566 | | result = (List<WearableItem>)await sourceToAwait.Task; |
| | 567 | |
|
| 2 | 568 | | ct.ThrowIfCancellationRequested(); |
| | 569 | |
|
| 4 | 570 | | return result.FirstOrDefault(x => string.Equals(x.id, ExtendedUrnParser.GetShortenedUrn(newWearableId), |
| | 571 | | StringComparison.OrdinalIgnoreCase)); |
| 2 | 572 | | } |
| | 573 | |
|
| | 574 | | private List<WearableItem> ValidateWearables( |
| | 575 | | IEnumerable<WearableWithEntityResponseDto.ElementDto> wearableElements, |
| | 576 | | string contentBaseUrl, |
| | 577 | | string bundlesBaseUrl) |
| | 578 | | { |
| 4 | 579 | | List<WearableItem> wearables = new (); |
| | 580 | |
|
| 16 | 581 | | foreach (var item in wearableElements) |
| | 582 | | { |
| 4 | 583 | | EntityDto entity = item.entity; |
| 4 | 584 | | EntityDto.MetadataDto metadata = entity.metadata; |
| | 585 | |
|
| 4 | 586 | | if (IsInvalidWearable(metadata)) |
| | 587 | | continue; |
| | 588 | |
|
| | 589 | | try |
| | 590 | | { |
| 4 | 591 | | WearableItem wearable = item.ToWearableItem(contentBaseUrl, bundlesBaseUrl); |
| 4 | 592 | | wearables.Add(wearable); |
| 4 | 593 | | } |
| 0 | 594 | | catch (Exception e) { Debug.LogException(e); } |
| | 595 | | } |
| | 596 | |
|
| 4 | 597 | | return wearables; |
| | 598 | | } |
| | 599 | |
|
| | 600 | | private void MapLambdasDataIntoWearableItem(IList<WearableItem> wearablesFromLambdas) |
| | 601 | | { |
| 5 | 602 | | var invalidWearablesIndices = ListPool<int>.Get(); |
| | 603 | |
|
| 26 | 604 | | for (var i = 0; i < wearablesFromLambdas.Count; i++) |
| | 605 | | { |
| 8 | 606 | | var wearable = wearablesFromLambdas[i]; |
| | 607 | |
|
| 8 | 608 | | if (IsInvalidWearable(wearable)) |
| | 609 | | { |
| 0 | 610 | | invalidWearablesIndices.Add(i); |
| 0 | 611 | | continue; |
| | 612 | | } |
| | 613 | |
|
| | 614 | | try |
| | 615 | | { |
| 32 | 616 | | foreach (var representation in wearable.data.representations) |
| | 617 | | { |
| 32 | 618 | | foreach (var representationContent in representation.contents) |
| 8 | 619 | | representationContent.hash = representationContent.url[(representationContent.url.LastIndexO |
| | 620 | | } |
| | 621 | |
|
| 8 | 622 | | string thumbnail = wearable.thumbnail ?? ""; |
| 8 | 623 | | int index = thumbnail.LastIndexOf('/'); |
| 8 | 624 | | string newThumbnail = thumbnail[(index + 1)..]; |
| 8 | 625 | | string newBaseUrl = thumbnail[..(index + 1)]; |
| 8 | 626 | | wearable.thumbnail = newThumbnail; |
| 8 | 627 | | wearable.baseUrl = string.IsNullOrEmpty(newBaseUrl) ? $"{catalyst.contentUrl}contents/" : newBaseUrl |
| 8 | 628 | | wearable.baseUrlBundles = assetBundlesUrl; |
| 8 | 629 | | wearable.emoteDataV0 = null; |
| 8 | 630 | | } |
| | 631 | | catch (Exception e) |
| | 632 | | { |
| 0 | 633 | | Debug.LogException(e); |
| 0 | 634 | | invalidWearablesIndices.Add(i); |
| 0 | 635 | | } |
| | 636 | | } |
| | 637 | |
|
| 10 | 638 | | for (var i = 0; i < invalidWearablesIndices.Count; i++) |
| | 639 | | { |
| 0 | 640 | | int invalidWearablesIndex = invalidWearablesIndices[i] - i; |
| 0 | 641 | | wearablesFromLambdas.RemoveAt(invalidWearablesIndex); |
| | 642 | | } |
| | 643 | |
|
| 5 | 644 | | ListPool<int>.Release(invalidWearablesIndices); |
| 5 | 645 | | } |
| | 646 | |
|
| | 647 | | private static bool IsInvalidWearable(WearableItem item) |
| | 648 | | { |
| 8 | 649 | | if (string.IsNullOrEmpty(item.id)) |
| | 650 | | { |
| 0 | 651 | | Debug.LogError("Wearable is invalid: id is null"); |
| 0 | 652 | | return true; |
| | 653 | | } |
| | 654 | |
|
| 8 | 655 | | if (item.data.representations == null) |
| | 656 | | { |
| 0 | 657 | | Debug.LogError($"Wearable ${item.id} is invalid: data.representation is null"); |
| 0 | 658 | | return true; |
| | 659 | | } |
| | 660 | |
|
| 32 | 661 | | foreach (var dataRepresentation in item.data.representations) |
| | 662 | | { |
| 32 | 663 | | foreach (var representationContent in dataRepresentation.contents) |
| | 664 | | { |
| 8 | 665 | | if (string.IsNullOrEmpty(representationContent.url)) |
| | 666 | | { |
| 0 | 667 | | Debug.LogError("Wearable is invalid: representation content URL is null"); |
| 0 | 668 | | return true; |
| | 669 | | } |
| | 670 | | } |
| | 671 | | } |
| | 672 | |
|
| 8 | 673 | | return false; |
| | 674 | | } |
| | 675 | |
|
| | 676 | | private bool IsInvalidWearable(EntityDto.MetadataDto metadata) |
| | 677 | | { |
| 4 | 678 | | if (metadata == null) |
| | 679 | | { |
| 0 | 680 | | Debug.LogError("Wearable is invalid: metadata is null"); |
| 0 | 681 | | return true; |
| | 682 | | } |
| | 683 | |
|
| 4 | 684 | | if (string.IsNullOrEmpty(metadata.id)) |
| | 685 | | { |
| 0 | 686 | | Debug.LogError("Wearable is invalid: id is null"); |
| 0 | 687 | | return true; |
| | 688 | | } |
| | 689 | |
|
| 4 | 690 | | if (metadata.data.representations == null) |
| | 691 | | { |
| 0 | 692 | | Debug.LogError($"Wearable ${metadata.id} is invalid: data.representation is null"); |
| 0 | 693 | | return true; |
| | 694 | | } |
| | 695 | |
|
| 16 | 696 | | foreach (var representation in metadata.data.representations) |
| | 697 | | { |
| 16 | 698 | | foreach (string content in representation.contents) |
| | 699 | | { |
| 4 | 700 | | if (string.IsNullOrEmpty(content)) |
| | 701 | | { |
| 0 | 702 | | Debug.LogError("Wearable is invalid: representation content URL is null"); |
| 0 | 703 | | return true; |
| | 704 | | } |
| | 705 | | } |
| | 706 | | } |
| | 707 | |
|
| 4 | 708 | | return false; |
| | 709 | | } |
| | 710 | |
|
| | 711 | | private bool IsLocalPreview() => |
| 4 | 712 | | dataStore.realm.playerRealm.Get()?.serverName?.Equals("LocalPreview", StringComparison.OrdinalIgnoreCase) ?? |
| | 713 | |
|
| | 714 | | private string GetBuilderDomainUrl() |
| | 715 | | { |
| 0 | 716 | | string domain = kernelConfig.Get().builderUrl; |
| | 717 | |
|
| 0 | 718 | | if (string.IsNullOrEmpty(domain)) |
| 0 | 719 | | domain = "https://builder-api.decentraland.org/v1"; |
| 0 | 720 | | return domain; |
| | 721 | | } |
| | 722 | | } |
| | 723 | | } |