< Summary

Class:WearableItem
Assembly:AvatarAssets
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/WearableItem.cs
Covered lines:61
Uncovered lines:66
Coverable lines:127
Total lines:504
Line coverage:48% (61 of 127)
Covered branches:0
Total branches:0
Covered methods:14
Total methods:29
Method coverage:48.2% (14 of 29)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
WearableItem()0%110100%
WearableItem()0%110100%
TryGetRepresentation(...)0%2100%
GetRepresentation(...)0%660100%
GetContentProvider(...)0%4.024088.89%
CreateContentProvider(...)0%6200%
SupportsBodyShape(...)0%660100%
GetReplacesList(...)0%5.395075%
GetHidesList(...)0%13.3313087.5%
SanitizeHidesLists()0%440100%
DoesHide(...)0%2100%
IsCollectible()0%6200%
IsSkin()0%110100%
IsSmart()0%9.089090%
GetName(...)0%440100%
GetIssuedCountFromRarity(...)0%56700%
ComposeThumbnailUrl()0%110100%
ComposeHiddenCategories(...)0%30500%
ComposeHiddenCategoriesOrdered(...)0%1321100%
IsInL2()0%20400%
IsEmote()0%2100%
GetNftInfo()0%30500%
ShowInBackpack()0%2100%
ToString()0%2100%
GetMarketplaceLink()0%30500%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/WearableItem.cs

#LineLine coverage
 1using DCL;
 2using DCL.Emotes;
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using UnityEngine;
 7using static WearableLiterals;
 8
 9// TODO: We need to separate this entity into WearableItem and EmoteItem they both can inherit a EntityItem and just hav
 10[Serializable]
 11public class WearableItem
 12{
 13    private const string THIRD_PARTY_COLLECTIONS_PATH = "collections-thirdparty";
 114    public static readonly IList<string> CATEGORIES_PRIORITY = new List<string>
 15    {
 16        Categories.SKIN,
 17        Categories.UPPER_BODY,
 18        Categories.HANDS_WEAR,
 19        Categories.LOWER_BODY,
 20        Categories.FEET,
 21        Categories.HELMET,
 22        Categories.HAT,
 23        Categories.TOP_HEAD,
 24        Categories.MASK,
 25        Categories.EYEWEAR,
 26        Categories.EARRING,
 27        Categories.TIARA,
 28        Categories.HAIR,
 29        Categories.EYEBROWS,
 30        Categories.EYES,
 31        Categories.MOUTH,
 32        Categories.FACIAL_HAIR,
 33        Categories.BODY_SHAPE,
 34    };
 35
 136    public static readonly Dictionary<string, string> CATEGORIES_READABLE_MAPPING = new ()
 37    {
 38        { Categories.SKIN, "Skin" },
 39        { Categories.UPPER_BODY, "Upper body" },
 40        { Categories.LOWER_BODY, "Lower body" },
 41        { Categories.FEET, "Feet" },
 42        { Categories.HELMET, "Helmet" },
 43        { Categories.HAT, "Hat" },
 44        { Categories.TOP_HEAD, "Top Head" },
 45        { Categories.MASK, "Mask" },
 46        { Categories.EYEWEAR, "Eyewear" },
 47        { Categories.EARRING, "Earring" },
 48        { Categories.TIARA, "Tiara" },
 49        { Categories.EYES, "Eyes" },
 50        { Categories.MOUTH, "Mouth" },
 51        { Categories.HAIR, "Hair" },
 52        { Categories.EYEBROWS, "Eyebrows" },
 53        { Categories.BODY_SHAPE, "Body shape" },
 54        { Categories.FACIAL_HAIR, "Facial hair" },
 55        { Categories.HANDS_WEAR, "Handwear" },
 56    };
 57
 158    public static readonly string[] SKIN_IMPLICIT_CATEGORIES =
 59    {
 60        Categories.EYES,
 61        Categories.MOUTH,
 62        Categories.EYEBROWS,
 63        Categories.HAIR,
 64        Categories.UPPER_BODY,
 65        Categories.LOWER_BODY,
 66        Categories.FEET,
 67        Categories.HANDS,
 68        Categories.HANDS_WEAR,
 69        Categories.HEAD,
 70        Categories.FACIAL_HAIR
 71    };
 72
 173    public static readonly string[] UPPER_BODY_DEFAULT_HIDES =
 74    {
 75        Categories.HANDS,
 76    };
 77
 78    [Serializable]
 79    public class MappingPair
 80    {
 81        public string key;
 82        public string hash;
 83        public string url;
 84    }
 85
 86    [Serializable]
 87    public class Representation
 88    {
 89        public string[] bodyShapes;
 90        public string mainFile;
 91        public MappingPair[] contents;
 92        public string[] overrideHides;
 93        public string[] overrideReplaces;
 94    }
 95
 96    [Serializable]
 97    public class Data
 98    {
 99        public Representation[] representations;
 100        public string category;
 101        public string[] tags;
 102        public string[] replaces;
 103        public string[] hides;
 104        public string[] removesDefaultHiding;
 105        public bool loop;
 106        public bool blockVrmExport;
 107    }
 108
 109    public Data data;
 110    public EmoteDataV0 emoteDataV0;
 111    public string id; // urn
 112    public string entityId;
 113
 114    public string baseUrl;
 115    public string baseUrlBundles;
 116
 117    public i18n[] i18n;
 118    public string thumbnail;
 119
 38120    public DateTime MostRecentTransferredDate { get; set; }
 121
 122    private string thirdPartyCollectionId;
 123
 124    public string ThirdPartyCollectionId
 125    {
 126        get
 127        {
 0128            if (!string.IsNullOrEmpty(thirdPartyCollectionId))
 0129                return thirdPartyCollectionId;
 130
 0131            if (!id.Contains(THIRD_PARTY_COLLECTIONS_PATH))
 0132                return "";
 133
 0134            var paths = id.Split(':');
 0135            var thirdPartyIndex = Array.IndexOf(paths, THIRD_PARTY_COLLECTIONS_PATH);
 0136            thirdPartyCollectionId = string.Join(":", paths, 0, thirdPartyIndex + 2);
 0137            return thirdPartyCollectionId;
 138        }
 139    }
 140
 0141    public bool IsFromThirdPartyCollection => !string.IsNullOrEmpty(ThirdPartyCollectionId);
 142
 143    public Sprite thumbnailSprite;
 144
 145    //This fields are temporary, once Kernel is finished we must move them to wherever they are placed
 146    public int amount;
 147    public string rarity;
 148    public string description;
 149    public int issuedId;
 150
 2983151    private Dictionary<string, string> cachedI18n = new ();
 2983152    private Dictionary<string, ContentProvider> cachedContentProviers = new ();
 153
 154    public bool TryGetRepresentation(string bodyshapeId, out Representation representation)
 155    {
 0156        representation = GetRepresentation(bodyshapeId);
 0157        return representation != null;
 158    }
 159
 160    public Representation GetRepresentation(string bodyShapeType)
 161    {
 74162        if (data?.representations == null)
 2163            return null;
 164
 288165        for (int i = 0; i < data.representations.Length; i++)
 166        {
 104167            if (data.representations[i].bodyShapes.Contains(bodyShapeType)) { return data.representations[i]; }
 168        }
 169
 56170        return null;
 171    }
 172
 173    public ContentProvider GetContentProvider(string bodyShapeType)
 174    {
 2175        var representation = GetRepresentation(bodyShapeType);
 176
 2177        if (representation == null)
 0178            return null;
 179
 2180        cachedContentProviers ??= new Dictionary<string, ContentProvider>();
 181
 2182        if (!cachedContentProviers.ContainsKey(bodyShapeType))
 183        {
 1184            var contentProvider = CreateContentProvider(baseUrl, representation.contents);
 1185            contentProvider.BakeHashes();
 1186            cachedContentProviers.Add(bodyShapeType, contentProvider);
 187        }
 188
 2189        return cachedContentProviers[bodyShapeType];
 190    }
 191
 192    protected virtual ContentProvider CreateContentProvider(string baseUrl, MappingPair[] contents)
 193    {
 0194        return new ContentProvider
 195        {
 196            baseUrl = baseUrl,
 197            assetBundlesBaseUrl = baseUrlBundles,
 0198            contents = contents.Select(mapping => new ContentServerUtils.MappingPair()
 199                                    { file = mapping.key, hash = mapping.hash })
 200                               .ToList()
 201        };
 202    }
 203
 204    public bool SupportsBodyShape(string bodyShapeType)
 205    {
 19206        if (data?.representations == null)
 7207            return false;
 208
 44209        for (int i = 0; i < data.representations.Length; i++)
 210        {
 14211            if (data.representations[i].bodyShapes.Contains(bodyShapeType)) { return true; }
 212        }
 213
 10214        return false;
 215    }
 216
 217    public string[] GetReplacesList(string bodyShapeType)
 218    {
 35219        var representation = GetRepresentation(bodyShapeType);
 220
 35221        if (representation?.overrideReplaces == null || representation.overrideReplaces.Length == 0)
 35222            return data.replaces;
 223
 0224        return representation.overrideReplaces;
 225    }
 226
 227    public string[] GetHidesList(string bodyShapeType)
 228    {
 35229        var representation = GetRepresentation(bodyShapeType);
 230
 35231        HashSet<string> hides = new HashSet<string>();
 232
 35233        if (representation?.overrideHides == null || representation.overrideHides.Length == 0)
 35234            hides.UnionWith(data.hides ?? Enumerable.Empty<string>());
 235        else
 0236            hides.UnionWith(representation.overrideHides);
 237
 35238        if (IsSkin())
 0239            hides.UnionWith(SKIN_IMPLICIT_CATEGORIES);
 240
 241        // we apply this rule to hide the hands by default if the wearable is an upper body or hides the upper body
 35242        bool isOrHidesUpperBody = hides.Contains(Categories.UPPER_BODY) || data.category == Categories.UPPER_BODY;
 243
 244        // the rule is ignored if the wearable contains the removal of this default rule (newer upper bodies since the r
 35245        bool removesHandDefault = data.removesDefaultHiding?.Contains(Categories.HANDS) ?? false;
 246
 247        // why we do this? because old upper bodies contains the base hand mesh, and they might clip with the new handwe
 35248        if (isOrHidesUpperBody && !removesHandDefault)
 11249            hides.UnionWith(UPPER_BODY_DEFAULT_HIDES);
 250
 35251        string[] replaces = GetReplacesList(bodyShapeType);
 252
 35253        if (replaces != null)
 35254            hides.UnionWith(replaces);
 255
 256        // Safeguard so no wearable can hide itself
 35257        hides.Remove(data.category);
 258
 35259        return hides.ToArray();
 260    }
 261
 262    public void SanitizeHidesLists()
 263    {
 264        //remove bodyshape from hides list
 28265        if (data.hides != null)
 10266            data.hides = data.hides.Except(new[] { Categories.BODY_SHAPE }).ToArray();
 267
 112268        for (int i = 0; i < data.representations.Length; i++)
 269        {
 28270            Representation representation = data.representations[i];
 271
 28272            if (representation.overrideHides != null)
 11273                representation.overrideHides = representation.overrideHides.Except(new[] { Categories.BODY_SHAPE }).ToAr
 274        }
 28275    }
 276
 277    public bool DoesHide(string category, string bodyShape) =>
 0278        GetHidesList(bodyShape).Any(s => s == category);
 279
 280    public bool IsCollectible()
 281    {
 0282        if (id == null)
 0283            return false;
 284
 0285        return !id.StartsWith("urn:decentraland:off-chain:base-avatars:");
 286    }
 287
 288    public bool IsSkin() =>
 35289        data.category == Categories.SKIN;
 290
 291    public bool IsSmart()
 292    {
 21293        if (data?.representations == null)
 14294            return false;
 295
 28296        for (var i = 0; i < data.representations.Length; i++)
 297        {
 7298            var representation = data.representations[i];
 7299            var containsGameJs = representation.contents?.Any(pair => pair.key.EndsWith("game.js")) ?? false;
 300
 7301            if (containsGameJs)
 0302                return true;
 303        }
 304
 7305        return false;
 306    }
 307
 308    public string GetName(string langCode = "en")
 309    {
 1310        cachedI18n ??= new Dictionary<string, string>();
 2311        cachedI18n.TryAdd(langCode, i18n.FirstOrDefault(x => x.code == langCode)?.text);
 1312        return cachedI18n[langCode];
 313    }
 314
 315    public int GetIssuedCountFromRarity(string rarity)
 316    {
 317        switch (rarity)
 318        {
 319            case ItemRarity.RARE:
 0320                return 5000;
 321            case ItemRarity.EPIC:
 0322                return 1000;
 323            case ItemRarity.LEGENDARY:
 0324                return 100;
 325            case ItemRarity.MYTHIC:
 0326                return 10;
 327            case ItemRarity.EXOTIC:
 0328                return 50;
 329            case ItemRarity.UNIQUE:
 0330                return 1;
 331        }
 332
 0333        return int.MaxValue;
 334    }
 335
 336    public string ComposeThumbnailUrl()
 337    {
 22338        return baseUrl + thumbnail;
 339    }
 340
 341    public static HashSet<string> ComposeHiddenCategories(string bodyShapeId, List<WearableItem> wearables)
 342    {
 0343        HashSet<string> result = new HashSet<string>();
 344
 345        //Last wearable added has priority over the rest
 0346        for (int index = 0; index < wearables.Count; index++)
 347        {
 0348            WearableItem wearableItem = wearables[index];
 349
 0350            if (wearableItem == null)
 351                continue;
 352
 0353            if (result.Contains(wearableItem.data.category)) //Skip hidden elements to avoid two elements hiding each ot
 354                continue;
 355
 0356            string[] wearableHidesList = wearableItem.GetHidesList(bodyShapeId);
 357
 0358            if (wearableHidesList != null) { result.UnionWith(wearableHidesList); }
 359        }
 360
 0361        return result;
 362    }
 363
 364    public static HashSet<string> ComposeHiddenCategoriesOrdered(string bodyShapeId, HashSet<string> forceRender, List<W
 365    {
 0366        var result = new HashSet<string>();
 0367        var wearablesByCategory = new Dictionary<string, WearableItem>();
 368
 0369        foreach (var wearable in wearables)
 0370            wearablesByCategory.TryAdd(wearable.data.category, wearable);
 371
 0372        var previouslyHidden = new Dictionary<string, HashSet<string>>();
 373
 0374        foreach (string priorityCategory in CATEGORIES_PRIORITY)
 375        {
 0376            previouslyHidden[priorityCategory] = new HashSet<string>();
 377
 0378            if (!wearablesByCategory.TryGetValue(priorityCategory, out var wearable) || wearable.GetHidesList(bodyShapeI
 379                continue;
 380
 0381            foreach (string categoryToHide in wearable.GetHidesList(bodyShapeId))
 382            {
 0383                if (previouslyHidden.TryGetValue(categoryToHide, out var hiddenCategories) && hiddenCategories.Contains(
 384                    continue;
 385
 0386                previouslyHidden[priorityCategory].Add(categoryToHide);
 387
 0388                if (forceRender != null && forceRender.Contains(categoryToHide))
 389                    continue;
 390
 0391                result.Add(categoryToHide);
 392            }
 393        }
 394
 0395        return result;
 396    }
 397
 398    //Workaround to know the net of a wearable.
 399    //Once wearables are allowed to be moved from Ethereum to Polygon this method wont be reliable anymore
 400    //To retrieve this properly first we need the catalyst to send the net of each wearable, not just the ID
 401    public bool IsInL2()
 402    {
 0403        if (id.StartsWith("urn:decentraland:matic") ||
 404            id.StartsWith("urn:decentraland:mumbai") ||
 405            id.StartsWith("urn:decentraland:amoy"))
 0406            return true;
 407
 0408        return false;
 409    }
 410
 411    public bool IsEmote() =>
 0412        emoteDataV0 != null;
 413
 414    public NftInfo GetNftInfo() =>
 0415        new ()
 416        {
 417            Id = id,
 418            Category = IsEmote() ? "emote" : data?.category,
 419        };
 420
 421    public virtual bool ShowInBackpack() =>
 0422        true;
 423
 424    public override string ToString() =>
 0425        id;
 426
 427    public string GetMarketplaceLink()
 428    {
 0429        if (!IsCollectible())
 0430            return "";
 431
 432        const string MARKETPLACE = "https://market.decentraland.org/contracts/{0}/items/{1}";
 0433        var split = id.Split(":");
 434
 0435        if (split.Length < 2)
 0436            return "";
 437
 438        // If this is not correct, we could retrieve the marketplace link by checking TheGraph, but that's super slow
 0439        if (!split[^2].StartsWith("0x") || !int.TryParse(split[^1], out int _))
 0440            return "";
 441
 0442        return string.Format(MARKETPLACE, split[^2], split[^1]);
 443    }
 444}
 445
 446[Serializable]
 447public class EmoteItem : WearableItem
 448{
 449    public EmoteItem(string bodyShapeId, string emoteId, string emoteHash, string contentUrl, bool loop, bool needUrlApp
 450    {
 451        data = new Data
 452        {
 453            representations = new[]
 454            {
 455                new Representation
 456                {
 457                    bodyShapes = new[] { bodyShapeId },
 458                    contents = new[]
 459                    {
 460                        new MappingPair
 461                        {
 462                            hash = emoteHash, key = emoteHash
 463                        },
 464                    },
 465                    mainFile = emoteHash,
 466                },
 467            },
 468            loop = loop,
 469        };
 470
 471        emoteDataV0 = new EmoteDataV0 { loop = loop };
 472
 473        id = emoteId;
 474        baseUrl = needUrlAppend ? $"{contentUrl}contents/" : contentUrl;
 475    }
 476}
 477
 478[Serializable]
 479public class WearablesRequestResponse
 480{
 481    public WearableItem[] wearables;
 482    public string context;
 483}
 484
 485[Serializable]
 486public class WearablesRequestFailed
 487{
 488    public string error;
 489    public string context;
 490}
 491
 492[Serializable]
 493public class WearableContent
 494{
 495    public string file;
 496    public string hash;
 497}
 498
 499[Serializable]
 500public class i18n
 501{
 502    public string code;
 503    public string text;
 504}