diff --git a/Explorer/Assets/AddressableAssetsData/AssetGroups/Wearables.asset b/Explorer/Assets/AddressableAssetsData/AssetGroups/Wearables.asset index c03fae5a05..fe70e91647 100644 --- a/Explorer/Assets/AddressableAssetsData/AssetGroups/Wearables.asset +++ b/Explorer/Assets/AddressableAssetsData/AssetGroups/Wearables.asset @@ -22,6 +22,11 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 07f4f762821ea4a7e8740b3ac135b411 + m_Address: EmptyDefaultWearable + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: fc8a9d2b539788c47a5b305639fa8b34, type: 2} m_SchemaSet: diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarShape.asmdef b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarShape.asmdef index e86521a7f2..02b3a37190 100644 --- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarShape.asmdef +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarShape.asmdef @@ -22,7 +22,9 @@ "GUID:7f7d1af65c2641843945d409d28f2e20", "GUID:3640f3c0b42946b0b8794a1ed8e06ca5", "GUID:275e22790c04e9b47a5085d7b0c4432a", - "GUID:c80c82a8f4e04453b85fbab973d6774a" + "GUID:c80c82a8f4e04453b85fbab973d6774a", + "GUID:e56a0d6a94c144c784012e63b6043100", + "GUID:8322ea9340a544c59ddc56d4793eac74" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs index 02153450cc..43d8f597ce 100644 --- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs @@ -126,15 +126,10 @@ private AvatarCustomSkinningComponent InstantiateAvatar(ref AvatarShapeComponent continue; } - if (resultWearable.isFacialFeature()) - { - //TODO: Facial Features. They are textures that should be applied on the body shape, not gameobjects to instantiate. - //We need the asset bundle to have access to the texture - } - else - { - WearableAsset originalAsset = resultWearable.GetOriginalAsset(avatarShapeComponent.BodyShape); + WearableAsset originalAsset = resultWearable.GetOriginalAsset(avatarShapeComponent.BodyShape); + if (originalAsset.GameObject != null) + { CachedWearable instantiatedWearable = wearableAssetsCache.InstantiateWearable(originalAsset, avatarBase.transform); @@ -145,6 +140,18 @@ private AvatarCustomSkinningComponent InstantiateAvatar(ref AvatarShapeComponent if (resultWearable.IsBodyShape()) bodyShape = instantiatedWearable; } + else + { + if (resultWearable.IsFacialFeature()) + { + //TODO: Facial Features. They are textures that should be applied on the body shape, not gameobjects to instantiate. + //We need the asset bundle to have access to the texture + } + + //TODO: There are rare cases where the wearable should be a gameobject, but the asset bundle did not bring it + //This happened to me with bafkreiejnil6fhcb6s2pjbuvbwb6s7bo4flz4o4wosjjqrtd4gjuyht45u (aviator style eyewear) + //We should handle it + } } AvatarWearableHide.HideBodyShape(bodyShape, wearablesToHide, usedCategories); diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarLoaderSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarLoaderSystem.cs index 7dd33d1c8c..6fdae7550a 100644 --- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarLoaderSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarLoaderSystem.cs @@ -6,9 +6,13 @@ using DCL.AvatarRendering.Wearables.Helpers; using DCL.Diagnostics; using DCL.ECSComponents; +using DCL.Profiles; +using Decentraland.Common; using ECS.Abstract; using ECS.Prioritization.Components; using ECS.Unity.ColorComponent; +using UnityEngine; +using Entity = Arch.Core.Entity; using Promise = ECS.StreamableLoading.Common.AssetPromise< DCL.AvatarRendering.Wearables.Components.IWearable[], DCL.AvatarRendering.Wearables.Components.Intentions.GetWearablesByPointersIntention>; @@ -23,10 +27,12 @@ internal AvatarLoaderSystem(World world) : base(world) { } protected override void Update(float t) { + UpdateAvatarByProfileQuery(World); LoadNewAvatarQuery(World); UpdateAvatarQuery(World); } + // TODO: remove PBAvatarShape as middleware, instead use Profile directly [Query] [None(typeof(AvatarShapeComponent))] private void LoadNewAvatar(in Entity entity, ref PBAvatarShape pbAvatarShape, ref PartitionComponent partition) @@ -36,6 +42,7 @@ private void LoadNewAvatar(in Entity entity, ref PBAvatarShape pbAvatarShape, re World.Add(entity, new AvatarShapeComponent(pbAvatarShape.Name, pbAvatarShape.Id, pbAvatarShape, wearablePromise, pbAvatarShape.SkinColor.ToUnityColor(), pbAvatarShape.HairColor.ToUnityColor())); } + // TODO: remove PBAvatarShape as middleware, instead use Profile directly [Query] private void UpdateAvatar(ref PBAvatarShape pbAvatarShape, ref AvatarShapeComponent avatarShapeComponent, ref PartitionComponent partition) { @@ -53,6 +60,37 @@ private void UpdateAvatar(ref PBAvatarShape pbAvatarShape, ref AvatarShapeCompon pbAvatarShape.IsDirty = false; } + // TODO: remove PBAvatarShape as middleware, instead use Profile directly + [Query] + private void UpdateAvatarByProfile(in Entity entity, ref Profile profile, ref PBAvatarShape pbAvatarShape, ref PartitionComponent partition) + { + Color avatarSkinColor = profile.Avatar.SkinColor; + Color avatarHairColor = profile.Avatar.HairColor; + + World.Set(entity, new PBAvatarShape + { + Id = profile.UserId, + BodyShape = profile.Avatar.BodyShape, + Wearables = { profile.Avatar.SharedWearables }, + Name = profile.Name, + SkinColor = new Color3 + { + R = avatarSkinColor.r, + B = avatarSkinColor.b, + G = avatarSkinColor.g, + }, + HairColor = new Color3 + { + R = avatarHairColor.r, + B = avatarHairColor.b, + G = avatarHairColor.g, + }, + IsDirty = true, + }); + + World.Remove(entity); + } + private Promise CreateWearablePromise(PBAvatarShape pbAvatarShape, PartitionComponent partition) => Promise.Create(World, WearableComponentsUtils.CreateGetWearablesByPointersIntention(pbAvatarShape, pbAvatarShape.Wearables), diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/OwnAvatarLoaderFromDebugMenuSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/OwnAvatarLoaderFromDebugMenuSystem.cs new file mode 100644 index 0000000000..89423cd7c5 --- /dev/null +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/OwnAvatarLoaderFromDebugMenuSystem.cs @@ -0,0 +1,61 @@ +using Arch.Core; +using Arch.SystemGroups; +using Arch.SystemGroups.DefaultSystemGroups; +using DCL.DebugUtilities; +using DCL.DebugUtilities.UIBindings; +using DCL.Diagnostics; +using DCL.Profiles; +using ECS; +using ECS.Abstract; +using ECS.Prioritization.Components; +using ECS.StreamableLoading.Common.Components; +using System.Threading; +using Promise = ECS.StreamableLoading.Common.AssetPromise< + DCL.Profiles.Profile, + DCL.Profiles.GetProfileIntention>; + +namespace DCL.AvatarRendering.AvatarShape.Systems +{ + [UpdateInGroup(typeof(PresentationSystemGroup))] + [LogCategory(ReportCategory.AVATAR)] + public partial class OwnAvatarLoaderFromDebugMenuSystem : BaseUnityLoopSystem + { + private readonly Entity ownPlayerEntity; + private readonly IRealmData realmData; + private readonly DebugWidgetVisibilityBinding widgetVisibility; + + private CancellationTokenSource fetchProfileCancellationToken; + + public OwnAvatarLoaderFromDebugMenuSystem( + World world, + Entity ownPlayerEntity, + IDebugContainerBuilder debugContainerBuilder, + IRealmData realmData) + : base(world) + { + this.ownPlayerEntity = ownPlayerEntity; + this.realmData = realmData; + + debugContainerBuilder.AddWidget("Profile: Avatar Shape") + .SetVisibilityBinding(widgetVisibility = new DebugWidgetVisibilityBinding(false)) + .AddStringFieldWithConfirmation("0x..", "Set Address", UpdateProfileForOwnAvatar); + } + + protected override void Update(float t) + { + widgetVisibility.SetVisible(realmData.Configured); + } + + private void UpdateProfileForOwnAvatar(string profileId) + { + const int VERSION = 0; + + var promise = Promise.Create(World, + new GetProfileIntention(profileId, VERSION, + new CommonLoadingArguments($"profiles/{profileId}?version={VERSION}")), + PartitionComponent.TOP_PRIORITY); + + World.Add(ownPlayerEntity, promise); + } + } +} diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/OwnAvatarLoaderFromDebugMenuSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/OwnAvatarLoaderFromDebugMenuSystem.cs.meta new file mode 100644 index 0000000000..4d2cf2dbb6 --- /dev/null +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/OwnAvatarLoaderFromDebugMenuSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6423ad093e664747b2038c55742b7264 +timeCreated: 1702563158 \ No newline at end of file diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Assets/EmptyDefaultWearable.prefab b/Explorer/Assets/DCL/AvatarRendering/Wearables/Assets/EmptyDefaultWearable.prefab new file mode 100644 index 0000000000..c046058e29 --- /dev/null +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Assets/EmptyDefaultWearable.prefab @@ -0,0 +1,33 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1300342356663069903 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6132992296668838597} + m_Layer: 0 + m_Name: EmptyDefaultWearable + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6132992296668838597 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1300342356663069903} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Assets/EmptyDefaultWearable.prefab.meta b/Explorer/Assets/DCL/AvatarRendering/Wearables/Assets/EmptyDefaultWearable.prefab.meta new file mode 100644 index 0000000000..90999e28f4 --- /dev/null +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Assets/EmptyDefaultWearable.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 07f4f762821ea4a7e8740b3ac135b411 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs index 8c92ed924e..ad6e27a86e 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs @@ -34,6 +34,6 @@ public interface IWearable WearableDTO.WearableMetadataDto.DataDto GetData(); - bool isFacialFeature(); + bool IsFacialFeature(); } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs index 33bae03019..a7e6afc645 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs @@ -95,7 +95,7 @@ public void GetHidingList(string bodyShapeType, HashSet hideListResult) public WearableDTO.WearableMetadataDto.DataDto GetData() => WearableDTO.Asset.metadata.data; - public bool isFacialFeature() => + public bool IsFacialFeature() => WearablesConstants.FACIAL_FEATURES.Contains(GetCategory()); public bool IsCompatibleWithBodyShape(string bodyShape) diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableAssetsCache.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableAssetsCache.cs index 8dec509206..91dad1efa9 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableAssetsCache.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableAssetsCache.cs @@ -4,7 +4,7 @@ namespace DCL.AvatarRendering.Wearables.Helpers { public interface IWearableAssetsCache { - int WearablesAssesCount { get; } + int WearablesAssetsCount { get; } bool TryGet(WearableAsset asset, out CachedWearable instance); diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableCatalog.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableCatalog.cs index 9bb5cd1e99..8d0c7ff281 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableCatalog.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/IWearableCatalog.cs @@ -12,8 +12,9 @@ public interface IWearableCatalog /// Retrieves a wearable by its DTO or adds a new one if it doesn't exist. /// /// The wearable DTO + /// If true, the wearable will be added to the cache. /// An instance of the type. - IWearable GetOrAddWearableByDTO(WearableDTO wearableDto); + IWearable GetOrAddWearableByDTO(WearableDTO wearableDto, bool addToCache = true); /// /// Adds an empty wearable to the catalog. diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAsset.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAsset.cs index ad35b97810..bea4de1675 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAsset.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAsset.cs @@ -53,7 +53,7 @@ public void Dispose() disposed = true; RENDERER_INFO_POOL.Release(rendererInfos); - assetBundleData.Dereference(); + assetBundleData?.Dereference(); if (ReferenceCount > 0) ProfilingCounters.WearablesAssetsReferencedAmount.Value--; diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAssetsCache.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAssetsCache.cs index 3f22e6a68b..a446512b1e 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAssetsCache.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableAssetsCache.cs @@ -25,7 +25,7 @@ public class WearableAssetsCache : IWearableAssetsCache, IDisposable private readonly Transform parentContainer; private readonly SimplePriorityQueue unloadQueue = new (); - public int WearablesAssesCount => cache.Count; + public int WearablesAssetsCount => cache.Keys.Count; internal Dictionary> cache { get; } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableCatalog.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableCatalog.cs index ca85d3bfc7..eac7997d0e 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableCatalog.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearableCatalog.cs @@ -13,22 +13,24 @@ public partial class WearableCatalog : IWearableCatalog internal Dictionary wearablesCache { get; } = new (); - public IWearable GetOrAddWearableByDTO(WearableDTO wearableDto) => + public IWearable GetOrAddWearableByDTO(WearableDTO wearableDto, bool addToCache = true) => TryGetWearable(wearableDto.metadata.id, out IWearable existingWearable) ? existingWearable : AddWearable(wearableDto.metadata.id, new Wearable { WearableDTO = new StreamableLoadingResult(wearableDto), IsLoading = false, - }); + }, addToCache); public void AddEmptyWearable(string loadingIntentionPointer) => AddWearable(loadingIntentionPointer, new Wearable()); - internal IWearable AddWearable(string loadingIntentionPointer, IWearable wearable) + internal IWearable AddWearable(string loadingIntentionPointer, IWearable wearable, bool addToCache = true) { wearablesCache.Add(loadingIntentionPointer, wearable); - cacheKeysDictionary[loadingIntentionPointer] = listedCacheKeys.AddLast((loadingIntentionPointer, MultithreadingUtility.FrameCount)); + + if (addToCache) + cacheKeysDictionary[loadingIntentionPointer] = listedCacheKeys.AddLast((loadingIntentionPointer, MultithreadingUtility.FrameCount)); return wearable; } @@ -44,14 +46,6 @@ public bool TryGetWearable(string wearableURN, out IWearable wearable) return false; } - public IWearable GetDefaultWearable(BodyShape bodyShape, string category) - { - string wearableURN = WearablesConstants.DefaultWearables.GetDefaultWearable(bodyShape, category); - - UpdateListedCachePriority(@for: wearableURN); - return wearablesCache[wearableURN]; - } - private void UpdateListedCachePriority(string @for) { if (cacheKeysDictionary.TryGetValue(@for, out LinkedListNode<(string key, long lastUsedFrame)> node)) @@ -76,6 +70,9 @@ public void Unload(IConcurrentBudgetProvider frameTimeBudgetProvider) } } + public IWearable GetDefaultWearable(BodyShape bodyShape, string category) => + wearablesCache[WearablesConstants.DefaultWearables.GetDefaultWearable(bodyShape, category)]; + private static bool TryUnloadAllWearableAssets(IWearable wearable) { var countNullOrEmpty = 0; diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablesConstants.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablesConstants.cs index f9daa9f844..ac5d72ff2b 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablesConstants.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablesConstants.cs @@ -7,6 +7,8 @@ namespace DCL.AvatarRendering.Wearables.Helpers { public static class WearablesConstants { + public const string EMPTY_DEFAULT_WEARABLE = "EMPTY_DEFAULT_WEARABLE"; + //Used for hiding algorithm public static readonly IList CATEGORIES_PRIORITY = new List { @@ -96,9 +98,12 @@ private static readonly Color[] DEFAULT_SKIN_COLORS new (1.00f, 0.86f, 0.67f), }; - private static Color GetRandomSkinColor() => + public static Color GetRandomSkinColor() => DEFAULT_SKIN_COLORS[Random.Range(0, DEFAULT_SKIN_COLORS.Length)]; + public static Color GetRandomHairColor() => + Random.ColorHSV(); + public static Color3 GetRandomSkinColor3() { Color randomColor = GetRandomSkinColor(); @@ -146,7 +151,7 @@ public static string[] GetDefaultWearablesForBodyShape(string bodyShapeId) => public static string GetDefaultWearable(BodyShape bodyShapeId, string category) { if (!DEFAULT_WEARABLES.ContainsKey((bodyShapeId, category))) - return null; + return EMPTY_DEFAULT_WEARABLE; return DEFAULT_WEARABLES[(bodyShapeId, category)]; } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadDefaultWearablesSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadDefaultWearablesSystem.cs index 7a7681b228..5c21a2e1aa 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadDefaultWearablesSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadDefaultWearablesSystem.cs @@ -12,6 +12,7 @@ using ECS.StreamableLoading.Common; using ECS.StreamableLoading.Common.Components; using System.Collections.Generic; +using UnityEngine; namespace DCL.AvatarRendering.Wearables.Systems { @@ -21,12 +22,14 @@ public partial class LoadDefaultWearablesSystem : BaseUnityLoopSystem { private readonly WearablesDTOList defaultWearableDefinition; private readonly IWearableCatalog wearableCatalog; + private readonly GameObject emptyDefaultWearable; internal LoadDefaultWearablesSystem(World world, - WearablesDTOList defaultWearableDefinition, IWearableCatalog wearableCatalog) : base(world) + WearablesDTOList defaultWearableDefinition, IWearableCatalog wearableCatalog, GameObject emptyDefaultWearable) : base(world) { this.defaultWearableDefinition = defaultWearableDefinition; this.wearableCatalog = wearableCatalog; + this.emptyDefaultWearable = emptyDefaultWearable; } public override void Initialize() @@ -41,7 +44,7 @@ public override void Initialize() for (var i = 0; i < defaultWearableDefinition.Value.Count; i++) { WearableDTO dto = defaultWearableDefinition.Value[i]; - IWearable wearable = wearableCatalog.GetOrAddWearableByDTO(dto); + IWearable wearable = wearableCatalog.GetOrAddWearableByDTO(dto, false); BodyShape analyzedBodyShape = wearable.IsCompatibleWithBodyShape(BodyShape.MALE) ? BodyShape.MALE : BodyShape.FEMALE; pointersRequest[analyzedBodyShape].Add(wearable.GetUrn()); @@ -57,6 +60,25 @@ public override void Initialize() } World.Create(state); + + // Add empty default wearable + var wearableDTO = new WearableDTO + { + metadata = new WearableDTO.WearableMetadataDto + { + id = WearablesConstants.EMPTY_DEFAULT_WEARABLE, + }, + }; + + IWearable emptyWearable = wearableCatalog.GetOrAddWearableByDTO(wearableDTO, false); + var wearableAsset = new WearableAsset(emptyDefaultWearable, new List(), null); + wearableAsset.AddReference(); + + emptyWearable.WearableAssetResults[BodyShape.MALE] = + new StreamableLoadingResult(wearableAsset); + + emptyWearable.WearableAssetResults[BodyShape.FEMALE] = + new StreamableLoadingResult(wearableAsset); } protected override void Update(float t) diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadWearablesDTOByPointersSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadWearablesDTOByPointersSystem.cs index f4d0950224..7ca7b8f698 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadWearablesDTOByPointersSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/LoadWearablesDTOByPointersSystem.cs @@ -29,6 +29,7 @@ public partial class LoadWearablesDTOByPointersSystem : LoadSystemBase DTO_POOL = new (MAX_WEARABLES_PER_REQUEST, 50); private readonly StringBuilder bodyBuilder = new (); diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearableByPointerSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearableByPointerSystem.cs index c932930334..6f6b266b1c 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearableByPointerSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/ResolveWearableByPointerSystem.cs @@ -266,17 +266,31 @@ private void SetDefaultWearables(bool defaultWearablesLoaded, IWearable wearable return; } + //TODO: Add some client warning telling them that their asset cannot be loaded ReportHub.Log(GetReportCategory(), $"Request for wearable {wearable.GetHash()} failed, loading default wearable"); + IWearable defaultWearable = wearableCatalog.GetDefaultWearable(bodyShape, wearable.GetCategory()); - // This section assumes that the default wearables were successfully loaded. - // Waiting for the default wearable should be moved to the loading screen if (wearable.IsUnisex()) { - wearable.WearableAssetResults[BodyShape.MALE] = wearableCatalog.GetDefaultWearable(BodyShape.MALE, wearable.GetCategory()).WearableAssetResults[BodyShape.MALE]; - wearable.WearableAssetResults[BodyShape.FEMALE] = wearableCatalog.GetDefaultWearable(BodyShape.FEMALE, wearable.GetCategory()).WearableAssetResults[BodyShape.FEMALE]; + wearable.WearableAssetResults[BodyShape.MALE] = defaultWearable.WearableAssetResults[BodyShape.MALE]; + wearable.WearableAssetResults[BodyShape.FEMALE] = defaultWearable.WearableAssetResults[BodyShape.FEMALE]; } else - wearable.WearableAssetResults[bodyShape] = wearableCatalog.GetDefaultWearable(bodyShape, wearable.GetCategory()).WearableAssetResults[bodyShape]; + wearable.WearableAssetResults[bodyShape] = defaultWearable.WearableAssetResults[bodyShape]; + + // If the default wearable is empty, we need to remove all the hiding/replacing/removing data + if (defaultWearable.GetUrn().Equals(WearablesConstants.EMPTY_DEFAULT_WEARABLE)) + { + wearable.WearableDTO.Asset.metadata.data.hides = Array.Empty(); + wearable.WearableDTO.Asset.metadata.data.replaces = Array.Empty(); + wearable.WearableDTO.Asset.metadata.data.removesDefaultHiding = Array.Empty(); + + for (var i = 0; i < wearable.WearableDTO.Asset.metadata.data.representations.Length; i++) + { + wearable.WearableDTO.Asset.metadata.data.representations[i].overrideHides = Array.Empty(); + wearable.WearableDTO.Asset.metadata.data.representations[i].overrideReplaces = Array.Empty(); + } + } } private static void SetWearableResult(IWearable wearable, StreamableLoadingResult result, in BodyShape bodyShape) diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Tests/LoadDefaultWearablesSystemShould.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Tests/LoadDefaultWearablesSystemShould.cs index 1aac57cb5d..cfc40c98e2 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Tests/LoadDefaultWearablesSystemShould.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Tests/LoadDefaultWearablesSystemShould.cs @@ -25,7 +25,7 @@ public void Setup() var partialTargetList = new List(64); JsonConvert.PopulateObject(File.ReadAllText(definitionsPath), partialTargetList); - system = new LoadDefaultWearablesSystem(world, new WearablesDTOList(partialTargetList), new WearableCatalog()); + system = new LoadDefaultWearablesSystem(world, new WearablesDTOList(partialTargetList), new WearableCatalog(), new GameObject()); } [Test] diff --git a/Explorer/Assets/DCL/ECS/GlobalPartitioning/GlobalDeferredLoadingSystem.cs b/Explorer/Assets/DCL/ECS/GlobalPartitioning/GlobalDeferredLoadingSystem.cs index 9cd9bdb30f..b672871779 100644 --- a/Explorer/Assets/DCL/ECS/GlobalPartitioning/GlobalDeferredLoadingSystem.cs +++ b/Explorer/Assets/DCL/ECS/GlobalPartitioning/GlobalDeferredLoadingSystem.cs @@ -6,6 +6,7 @@ using DCL.AvatarRendering.Wearables.Helpers; using DCL.AvatarRendering.Wearables.Systems; using DCL.Optimization.PerformanceBudgeting; +using DCL.Profiles; using ECS.SceneLifeCycle.Components; using ECS.SceneLifeCycle.SceneDefinition; using ECS.SceneLifeCycle.Systems; @@ -43,6 +44,7 @@ static GlobalDeferredLoadingSystem() CreateQuery(), CreateQuery(), CreateQuery(), + CreateQuery(), }; } diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/BuilderExtensions.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/BuilderExtensions.cs index 03acd4178d..cf521bf6f2 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/BuilderExtensions.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/DebugUtilities/Builders/BuilderExtensions.cs @@ -22,6 +22,16 @@ public static DebugWidgetBuilder AddIntFieldWithConfirmation(this DebugWidgetBui return builder; } + public static DebugWidgetBuilder AddStringFieldWithConfirmation(this DebugWidgetBuilder builder, string defaultValue, string buttonName, Action onClick) + { + var binding = new ElementBinding(defaultValue); + var textFieldDef = new DebugTextFieldDef(binding); + + var buttonDef = new DebugButtonDef(buttonName, () => onClick?.Invoke(binding.Value)); + builder.AddControl(textFieldDef, buttonDef); + return builder; + } + public static DebugWidgetBuilder AddFloatField(this DebugWidgetBuilder builder, string labelName, ElementBinding elementBinding) { var label = new DebugConstLabelDef(labelName); diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs index 78c63a8a4d..3df69567fd 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs @@ -110,6 +110,8 @@ public static class ReportCategory /// public const string AVATAR = nameof(AVATAR); + public const string PROFILE = nameof(PROFILE); + /// /// Wearable related /// diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs index 71ffd54bb0..ebfe5a25ac 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs @@ -41,10 +41,10 @@ public static class new (MEMORY, "Wearables Assets In Catalog", ProfilerMarkerDataUnit.Count); public static ProfilerCounterValue CachedWearablesAmount = - new (MEMORY, "Cached Wearables", ProfilerMarkerDataUnit.Count); + new (MEMORY, "Instantiated Wearables", ProfilerMarkerDataUnit.Count); public static ProfilerCounterValue CachedWearablesInCacheAmount = - new (MEMORY, "Cached Wearables In Cache", ProfilerMarkerDataUnit.Count); + new (MEMORY, "Instantiated Wearables In Cache", ProfilerMarkerDataUnit.Count); public static ProfilerCounterValue GetWearablesIntentionAmount = new (MEMORY, "GetWearables Intentions", ProfilerMarkerDataUnit.Count); diff --git a/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef b/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef index b8f80479e3..4fc6a7dc66 100644 --- a/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef +++ b/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef @@ -53,7 +53,8 @@ "GUID:9b4463c7170cb485aaf17878a8b7281e", "GUID:5ab29fa8ae5769b49ab29e390caca7a4", "GUID:15bc5ce9b9f747c8a365e7dff4e2ebd7", - "GUID:393a5d2fbaa249d09c260018aaeb4fc0" + "GUID:393a5d2fbaa249d09c260018aaeb4fc0", + "GUID:e56a0d6a94c144c784012e63b6043100" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Explorer/Assets/DCL/PluginSystem/Global/Global Plugins Settings.asset b/Explorer/Assets/DCL/PluginSystem/Global/Global Plugins Settings.asset index e2e2fe56c0..b75953f611 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/Global Plugins Settings.asset +++ b/Explorer/Assets/DCL/PluginSystem/Global/Global Plugins Settings.asset @@ -130,6 +130,11 @@ MonoBehaviour: m_SubObjectName: m_SubObjectType: m_EditorAssetChanged: 0 + emptyDefaultWearable: + m_AssetGUID: 07f4f762821ea4a7e8740b3ac135b411 + m_SubObjectName: + m_SubObjectType: + m_EditorAssetChanged: 0 - rid: 6784895264912310272 type: {class: StaticSettings, ns: DCL.PluginSystem.Global, asm: DCL.Plugins} data: @@ -154,6 +159,9 @@ MonoBehaviour: m_SubObjectName: m_SubObjectType: m_EditorAssetChanged: 0 + k__BackingField: 33 + k__BackingField: 100 + k__BackingField: 50 - rid: 6784895265412481026 type: {class: CharacterCameraSettings, ns: DCL.PluginSystem.Global, asm: DCL.Plugins} data: diff --git a/Explorer/Assets/DCL/PluginSystem/Global/ProfilePlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/ProfilePlugin.cs new file mode 100644 index 0000000000..83ddcf3eef --- /dev/null +++ b/Explorer/Assets/DCL/PluginSystem/Global/ProfilePlugin.cs @@ -0,0 +1,36 @@ +using Arch.SystemGroups; +using Cysharp.Threading.Tasks; +using DCL.Profiles; +using ECS.StreamableLoading.Cache; +using System; +using System.Threading; +using Utility.Multithreading; + +namespace DCL.PluginSystem.Global +{ + public class ProfilePlugin : IDCLGlobalPlugin + { + private readonly IProfileRepository profileRepository; + private IDCLGlobalPlugin idclGlobalPluginImplementation; + + public ProfilePlugin(IProfileRepository profileRepository) + { + this.profileRepository = profileRepository; + } + + public void Dispose() { } + + public UniTask Initialize(IPluginSettingsContainer container, CancellationToken ct) => + UniTask.CompletedTask; + + public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in GlobalPluginArguments arguments) + { + // not synced by mutex, for compatibility only + var mutexSync = new MutexSync(); + + LoadProfileSystem.InjectToWorld(ref builder, + new NoCache(false, false), + mutexSync, profileRepository); + } + } +} diff --git a/Explorer/Assets/DCL/PluginSystem/Global/ProfilePlugin.cs.meta b/Explorer/Assets/DCL/PluginSystem/Global/ProfilePlugin.cs.meta new file mode 100644 index 0000000000..e697e81e9f --- /dev/null +++ b/Explorer/Assets/DCL/PluginSystem/Global/ProfilePlugin.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8187424df2774755a99a176544eca71f +timeCreated: 1702585966 \ No newline at end of file diff --git a/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs index b945e9d37b..7da43b56b4 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.Threading; using UnityEngine; +using UnityEngine.AddressableAssets; using Utility.Multithreading; namespace DCL.AvatarRendering.Wearables @@ -37,6 +38,7 @@ public class WearablePlugin : IDCLGlobalPlugin private readonly IWearableCatalog wearableCatalog; private WearablesDTOList defaultWearablesDTOs; + private GameObject emptyDefaultWearable; public WearablePlugin(IAssetsProvisioner assetsProvisioner, IWebRequestController webRequestController, IRealmData realmData, URLDomain assetBundleURL, CacheCleaner cacheCleaner) { @@ -58,6 +60,9 @@ public async UniTask InitializeAsync(WearableSettings settings, CancellationToke JsonConvert.PopulateObject(defaultWearableDefinition.Value.text, partialTargetList); defaultWearablesDTOs = new WearablesDTOList(partialTargetList); + + ProvidedAsset emptyDefaultWearableAsset = await assetsProvisioner.ProvideMainAssetAsync(settings.emptyDefaultWearable, ct); + emptyDefaultWearable = emptyDefaultWearableAsset.Value; } public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in GlobalPluginArguments arguments) @@ -69,7 +74,7 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in GlobalP LoadWearablesByParamSystem.InjectToWorld(ref builder, webRequestController, new NoCache(false, false), realmData, EXPLORER_SUBDIRECTORY, WEARABLES_COMPLEMENT_URL, wearableCatalog, mutexSync); LoadWearablesDTOByPointersSystem.InjectToWorld(ref builder, webRequestController, new NoCache(false, false), mutexSync); LoadWearableAssetBundleManifestSystem.InjectToWorld(ref builder, webRequestController, new NoCache(true, true), mutexSync, assetBundleURL); - LoadDefaultWearablesSystem.InjectToWorld(ref builder, defaultWearablesDTOs, wearableCatalog); + LoadDefaultWearablesSystem.InjectToWorld(ref builder, defaultWearablesDTOs, wearableCatalog, emptyDefaultWearable); } [Serializable] @@ -77,6 +82,9 @@ public class WearableSettings : IDCLPluginSettings { [field: SerializeField] public AssetReferenceTextAsset defaultWearablesDefinition; + + [field: SerializeField] + public AssetReferenceGameObject emptyDefaultWearable; } } } diff --git a/Explorer/Assets/DCL/Profiles.meta b/Explorer/Assets/DCL/Profiles.meta new file mode 100644 index 0000000000..2e0f6806aa --- /dev/null +++ b/Explorer/Assets/DCL/Profiles.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f876e4b3fbd34e938677efacaa1b0210 +timeCreated: 1702059980 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/Avatar.cs b/Explorer/Assets/DCL/Profiles/Avatar.cs new file mode 100644 index 0000000000..dcc52a0c5a --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/Avatar.cs @@ -0,0 +1,42 @@ +using CommunicationData.URLHelpers; +using System.Collections.Generic; +using UnityEngine; + +namespace DCL.Profiles +{ + public class Avatar + { + public string BodyShape { get; internal set; } + public IReadOnlyCollection SharedWearables { get; internal set; } + public IReadOnlyCollection UniqueWearables { get; internal set; } + public IReadOnlyCollection ForceRender { get; internal set; } + public IReadOnlyDictionary Emotes { get; internal set; } + public URLAddress FaceSnapshotUrl { get; internal set; } + public URLAddress BodySnapshotUrl { get; internal set; } + public Color EyesColor { get; internal set; } + public Color HairColor { get; internal set; } + public Color SkinColor { get; internal set; } + + public Avatar() { } + + public Avatar(string bodyShape, + IReadOnlyCollection sharedWearables, + IReadOnlyCollection uniqueWearables, + IReadOnlyCollection forceRender, + Dictionary emotes, + URLAddress faceSnapshotUrl, URLAddress bodySnapshotUrl, + Color eyesColor, Color hairColor, Color skinColor) + { + BodyShape = bodyShape; + SharedWearables = sharedWearables; + UniqueWearables = uniqueWearables; + ForceRender = forceRender; + Emotes = emotes; + FaceSnapshotUrl = faceSnapshotUrl; + BodySnapshotUrl = bodySnapshotUrl; + EyesColor = eyesColor; + HairColor = hairColor; + SkinColor = skinColor; + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/Avatar.cs.meta b/Explorer/Assets/DCL/Profiles/Avatar.cs.meta new file mode 100644 index 0000000000..16cd961915 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/Avatar.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f60a155b8ab64f458e52c3a5b27acc15 +timeCreated: 1702300876 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/DCL.Profiles.asmdef b/Explorer/Assets/DCL/Profiles/DCL.Profiles.asmdef new file mode 100644 index 0000000000..d3889e3fdb --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/DCL.Profiles.asmdef @@ -0,0 +1,25 @@ +{ + "name": "DCL.Profiles", + "rootNamespace": "", + "references": [ + "GUID:f51ebe6a0ceec4240a699833d6309b23", + "GUID:4a12c0b1b77ec6b418a8d7bd5c925be3", + "GUID:e0eedfa2deb9406daf86fd8368728e39", + "GUID:8322ea9340a544c59ddc56d4793eac74", + "GUID:3640f3c0b42946b0b8794a1ed8e06ca5", + "GUID:4794e238ed0f65142a4aea5848b513e5", + "GUID:fa7b3fdbb04d67549916da7bd2af58ab", + "GUID:1b8e1e1bd01505f478f0369c04a4fb2f", + "GUID:275e22790c04e9b47a5085d7b0c4432a", + "GUID:101b8b6ebaf64668909b49c4b7a1420d" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/DCL.Profiles.asmdef.meta b/Explorer/Assets/DCL/Profiles/DCL.Profiles.asmdef.meta new file mode 100644 index 0000000000..3ef5005349 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/DCL.Profiles.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e56a0d6a94c144c784012e63b6043100 +timeCreated: 1702059989 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/DefaultProfileCache.cs b/Explorer/Assets/DCL/Profiles/DefaultProfileCache.cs new file mode 100644 index 0000000000..08118b3bba --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/DefaultProfileCache.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace DCL.Profiles +{ + public class DefaultProfileCache : IProfileCache + { + private readonly Dictionary profiles = new (); + + public Profile? Get(string id) => + profiles.ContainsKey(id) ? profiles[id] : null; + + public void Set(string id, Profile profile) => + profiles[id] = profile; + + public void Remove(string id) => + profiles.Remove(id); + } +} diff --git a/Explorer/Assets/DCL/Profiles/DefaultProfileCache.cs.meta b/Explorer/Assets/DCL/Profiles/DefaultProfileCache.cs.meta new file mode 100644 index 0000000000..326b55a948 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/DefaultProfileCache.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aec2ed419b024f69ac6fa6aa2171f487 +timeCreated: 1702508377 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/Emote.cs b/Explorer/Assets/DCL/Profiles/Emote.cs new file mode 100644 index 0000000000..acd54caeba --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/Emote.cs @@ -0,0 +1,14 @@ +namespace DCL.Profiles +{ + public class Emote + { + public int Slot { get; } + public string Urn { get; } + + public Emote(int slot, string urn) + { + Slot = slot; + Urn = urn; + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/Emote.cs.meta b/Explorer/Assets/DCL/Profiles/Emote.cs.meta new file mode 100644 index 0000000000..45a3d1d3f6 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/Emote.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6efd4099378f4fa19ec95320f9a42737 +timeCreated: 1702300883 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/GetProfileIntention.cs b/Explorer/Assets/DCL/Profiles/GetProfileIntention.cs new file mode 100644 index 0000000000..8a808236e5 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/GetProfileIntention.cs @@ -0,0 +1,34 @@ +using ECS.StreamableLoading.Common.Components; +using System; +using System.Threading; + +namespace DCL.Profiles +{ + public struct GetProfileIntention : ILoadingIntention, IEquatable + { + public string ProfileId { get; } + public int Version { get; } + public CancellationTokenSource CancellationTokenSource => CommonArguments.CancellationTokenSource; + public CommonLoadingArguments CommonArguments { get; set; } + + public GetProfileIntention(string profileId, int version, + CommonLoadingArguments commonArguments) + { + ProfileId = profileId; + Version = version; + CommonArguments = commonArguments; + } + + public bool Equals(GetProfileIntention other) => + ProfileId == other.ProfileId && Version == other.Version; + + public override bool Equals(object obj) => + obj is GetProfileIntention other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(ProfileId, Version); + + public override string ToString() => + $"Get Profile: {ProfileId} - {Version}"; + } +} diff --git a/Explorer/Assets/DCL/Profiles/GetProfileIntention.cs.meta b/Explorer/Assets/DCL/Profiles/GetProfileIntention.cs.meta new file mode 100644 index 0000000000..758d50e0ed --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/GetProfileIntention.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a13eaa51904748c99654d093336f3526 +timeCreated: 1702584789 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/IProfileCache.cs b/Explorer/Assets/DCL/Profiles/IProfileCache.cs new file mode 100644 index 0000000000..822426adc6 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/IProfileCache.cs @@ -0,0 +1,9 @@ +namespace DCL.Profiles +{ + public interface IProfileCache + { + Profile? Get(string id); + + void Set(string id, Profile profile); + } +} diff --git a/Explorer/Assets/DCL/Profiles/IProfileCache.cs.meta b/Explorer/Assets/DCL/Profiles/IProfileCache.cs.meta new file mode 100644 index 0000000000..5422fc0830 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/IProfileCache.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 230b50109b354783952b2bb2ffca1438 +timeCreated: 1702662031 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/IProfileRepository.cs b/Explorer/Assets/DCL/Profiles/IProfileRepository.cs new file mode 100644 index 0000000000..6182349c97 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/IProfileRepository.cs @@ -0,0 +1,10 @@ +using Cysharp.Threading.Tasks; +using System.Threading; + +namespace DCL.Profiles +{ + public interface IProfileRepository + { + UniTask GetAsync(string id, int version, CancellationToken ct); + } +} diff --git a/Explorer/Assets/DCL/Profiles/IProfileRepository.cs.meta b/Explorer/Assets/DCL/Profiles/IProfileRepository.cs.meta new file mode 100644 index 0000000000..5bfbc5a805 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/IProfileRepository.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f1effdf13ec84e438ea204a06a4a0435 +timeCreated: 1702066065 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/LoadProfileSystem.cs b/Explorer/Assets/DCL/Profiles/LoadProfileSystem.cs new file mode 100644 index 0000000000..37996f6d65 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/LoadProfileSystem.cs @@ -0,0 +1,63 @@ +using Arch.Core; +using Arch.System; +using Arch.SystemGroups; +using Arch.SystemGroups.DefaultSystemGroups; +using Cysharp.Threading.Tasks; +using DCL.Diagnostics; +using DCL.Optimization.PerformanceBudgeting; +using ECS.Prioritization.Components; +using ECS.StreamableLoading.Cache; +using ECS.StreamableLoading.Common; +using ECS.StreamableLoading.Common.Components; +using ECS.StreamableLoading.Common.Systems; +using System; +using System.Threading; +using Utility.Multithreading; + +namespace DCL.Profiles +{ + [UpdateInGroup(typeof(PresentationSystemGroup))] + [LogCategory(ReportCategory.PROFILE)] + public partial class LoadProfileSystem : LoadSystemBase + { + private readonly IProfileRepository profileRepository; + + public LoadProfileSystem(World world, + IStreamableCache cache, + MutexSync mutexSync, + IProfileRepository profileRepository) + : base(world, cache, mutexSync) + { + this.profileRepository = profileRepository; + } + + protected override void Update(float t) + { + base.Update(t); + + ResolveProfilePromiseQuery(World); + } + + protected override async UniTask> FlowInternalAsync(GetProfileIntention intention, + IAcquiredBudget acquiredBudget, IPartitionComponent partition, CancellationToken ct) + { + Profile? profile = await profileRepository.GetAsync(intention.ProfileId, intention.Version, ct); + + if (profile == null) + throw new Exception($"Profile not found {intention.ProfileId}"); + + return new StreamableLoadingResult(profile); + } + + [Query] + private void ResolveProfilePromise(in Entity entity, ref AssetPromise promise) + { + if (!promise.TryConsume(World, out StreamableLoadingResult result)) return; + + if (result.Succeeded) + World.Add(entity, result.Asset); + + World.Remove>(entity); + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/LoadProfileSystem.cs.meta b/Explorer/Assets/DCL/Profiles/LoadProfileSystem.cs.meta new file mode 100644 index 0000000000..049cd2b6df --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/LoadProfileSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6abc02d8506149ee8194e6c1c648a62c +timeCreated: 1702584759 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/Profile.cs b/Explorer/Assets/DCL/Profiles/Profile.cs new file mode 100644 index 0000000000..057182b327 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/Profile.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace DCL.Profiles +{ + public class Profile + { + public string UserId { get; internal set; } + public string Name { get; internal set; } + public string UnclaimedName { get; internal set; } + public bool HasClaimedName { get; internal set; } + public string Description { get; internal set; } + public int TutorialStep { get; internal set; } + public string Email { get; internal set; } + public int Version { get; internal set; } + public Avatar Avatar { get; internal set; } + public IReadOnlyCollection? Blocked { get; internal set; } + public IReadOnlyCollection? Interests { get; internal set; } + + internal Profile() { } + + public Profile(string userId, string name, string unclaimedName, bool hasClaimedName, string description, + int tutorialStep, string email, int version, Avatar avatar, IReadOnlyCollection blocked, + IReadOnlyCollection interests) + { + UserId = userId; + Name = name; + UnclaimedName = unclaimedName; + HasClaimedName = hasClaimedName; + Description = description; + TutorialStep = tutorialStep; + Email = email; + Version = version; + Avatar = avatar; + Blocked = blocked; + Interests = interests; + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/Profile.cs.meta b/Explorer/Assets/DCL/Profiles/Profile.cs.meta new file mode 100644 index 0000000000..a0e787ecbe --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/Profile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 66a10cefc9a6481faf141fc9d1b8f2a3 +timeCreated: 1702061933 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/ProfileJsonDto.cs b/Explorer/Assets/DCL/Profiles/ProfileJsonDto.cs new file mode 100644 index 0000000000..c09c4e2aef --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/ProfileJsonDto.cs @@ -0,0 +1,205 @@ +using CommunicationData.URLHelpers; +using DCL.Optimization.ThreadSafePool; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace DCL.Profiles +{ + [Serializable] + public class EmoteJsonDto + { + public int slot; + public string urn; + + public Emote ToEmote() => + new (slot, urn); + } + + [Serializable] + public class AvatarColorJsonDto + { + public float r; + public float g; + public float b; + public float a; + + public Color ToColor() => + new (r, g, b, a); + + public void Reset() + { + r = 1; + g = 1; + b = 1; + a = 1; + } + } + + [Serializable] + public class EyesJsonDto + { + public AvatarColorJsonDto? color; + } + + [Serializable] + public class HairJsonDto + { + public AvatarColorJsonDto? color; + } + + [Serializable] + public class SkinJsonDto + { + public AvatarColorJsonDto? color; + } + + [Serializable] + public class AvatarSnapshotJsonDto + { + public string face256; + public string body; + } + + [Serializable] + public class AvatarJsonDto + { + public string? bodyShape; + public List? wearables; + public List? forceRender; + public List? emotes; + public AvatarSnapshotJsonDto? snapshots; + public EyesJsonDto? eyes; + public HairJsonDto? hair; + public SkinJsonDto? skin; + + public void CopyTo(Avatar avatar) + { + const int SHARED_WEARABLES_MAX_URN_PARTS = 6; + + var sharedWearables = new HashSet(wearables.Count); + + foreach (string wearable in wearables) + sharedWearables.Add(wearable.ShortenURN(SHARED_WEARABLES_MAX_URN_PARTS)); + + // To avoid inconsistencies in the wearable references thus improving cache miss rate, + // we keep a list of shared wearables used by avatar shapes and most of the rendering systems + avatar.SharedWearables = sharedWearables; + // The wearables urns retrieved in the profile follows https://adr.decentraland.org/adr/ADR-244 + avatar.UniqueWearables = new HashSet(wearables); + avatar.BodyShape = bodyShape; + avatar.Emotes = emotes.ToDictionary(dto => dto.urn, dto => dto.ToEmote()); + avatar.FaceSnapshotUrl = URLAddress.FromString(snapshots.face256); + avatar.BodySnapshotUrl = URLAddress.FromString(snapshots.body); + avatar.EyesColor = eyes.color.ToColor(); + avatar.HairColor = hair.color.ToColor(); + avatar.SkinColor = skin.color.ToColor(); + } + + public void Reset() + { + bodyShape = default(string?); + wearables.Clear(); + forceRender?.Clear(); + emotes.Clear(); + snapshots.face256 = default(string); + snapshots.body = default(string); + eyes.color.Reset(); + hair.color.Reset(); + skin.color.Reset(); + } + } + + [Serializable] + public class ProfileJsonDto : IDisposable + { + private static readonly ThreadSafeObjectPool pool = new (() => new ProfileJsonDto()); + + public bool hasClaimedName; + public string description; + public int tutorialStep; + public string name; + public string userId; + public string email; + public string ethAddress; + public int version; + public AvatarJsonDto? avatar; + public List? blocked; + public List? interests; + public string unclaimedName; + public bool hasConnectedWeb3; + + public static ProfileJsonDto Create() + { + ProfileJsonDto profile = pool.Get(); + profile.Reset(); + return profile; + } + + public void Dispose() + { + pool.Release(this); + } + + public void CopyTo(Profile profile) + { + profile.UserId = userId; + profile.Name = name; + profile.UnclaimedName = unclaimedName; + profile.HasClaimedName = hasClaimedName; + profile.Description = description; + profile.TutorialStep = tutorialStep; + profile.Email = email; + profile.Version = version; + profile.Avatar ??= new Avatar(); + avatar?.CopyTo(profile.Avatar); + profile.Blocked = blocked != null ? new HashSet(blocked) : new HashSet(); + profile.Interests = interests != null ? new List(interests) : new List(); + } + + private void Reset() + { + hasClaimedName = default(bool); + description = default(string); + tutorialStep = default(int); + name = default(string); + userId = default(string); + email = default(string); + ethAddress = default(string); + version = default(int); + avatar?.Reset(); + blocked?.Clear(); + interests?.Clear(); + unclaimedName = default(string); + hasConnectedWeb3 = default(bool); + } + } + + [Serializable] + public class GetProfileJsonRootDto : IDisposable + { + private static readonly ThreadSafeObjectPool pool = new (() => new GetProfileJsonRootDto()); + + public long timestamp; + public List? avatars; + + public static GetProfileJsonRootDto Create() + { + GetProfileJsonRootDto root = pool.Get(); + root.avatars?.Clear(); + return root; + } + + private GetProfileJsonRootDto() { } + + public void Dispose() + { + if (avatars != null) + foreach (ProfileJsonDto avatar in avatars) + avatar.Dispose(); + + pool.Release(this); + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/ProfileJsonDto.cs.meta b/Explorer/Assets/DCL/Profiles/ProfileJsonDto.cs.meta new file mode 100644 index 0000000000..2b552dad32 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/ProfileJsonDto.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9da6192b47940fabc2446aa8cd99104 +timeCreated: 1702060160 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/ProfileParseException.cs b/Explorer/Assets/DCL/Profiles/ProfileParseException.cs new file mode 100644 index 0000000000..c50fd30a43 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/ProfileParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace DCL.Profiles +{ + public class ProfileParseException : Exception + { + public string ProfileId { get; } + public int Version { get; } + + public ProfileParseException(string profileId, int version, string json, Exception innerException) + : base($"Cannot parse profile: {profileId} - {version}\n{json}", innerException) + { + ProfileId = profileId; + Version = version; + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/ProfileParseException.cs.meta b/Explorer/Assets/DCL/Profiles/ProfileParseException.cs.meta new file mode 100644 index 0000000000..ea43e41cfc --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/ProfileParseException.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a6d6e3151cc44b80bfe8a7dd3aae99dd +timeCreated: 1702069456 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/RealmProfileRepository.cs b/Explorer/Assets/DCL/Profiles/RealmProfileRepository.cs new file mode 100644 index 0000000000..62880f7a35 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/RealmProfileRepository.cs @@ -0,0 +1,203 @@ +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.WebRequests; +using ECS; +using Ipfs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace DCL.Profiles +{ + public class RealmProfileRepository : IProfileRepository + { + private readonly IWebRequestController webRequestController; + private readonly IRealmData realm; + private readonly IProfileCache profileCache; + private readonly URLBuilder urlBuilder = new (); + + public RealmProfileRepository(IWebRequestController webRequestController, + IRealmData realm, + IProfileCache profileCache) + { + this.webRequestController = webRequestController; + this.realm = realm; + this.profileCache = profileCache; + } + + public async UniTask GetAsync(string id, int version, CancellationToken ct) + { + if (string.IsNullOrEmpty(id)) return null; + + IIpfsRealm ipfs = realm.Ipfs; + + urlBuilder.Clear(); + + urlBuilder.AppendDomain(ipfs.LambdasBaseUrl) + .AppendPath(URLPath.FromString($"profiles/{id}")) + .AppendParameter(new URLParameter("version", version.ToString())); + + URLAddress url = urlBuilder.Build(); + + try + { + GenericGetRequest response = await webRequestController.GetAsync(new CommonArguments(url), ct); + + using GetProfileJsonRootDto root = await response.CreateFromNewtonsoftJsonAsync( + createCustomExceptionOnFailure: (exception, text) => new ProfileParseException(id, version, text, exception), + converters: new ProfileJsonRootDtoConverter()); + + if (root.avatars == null) return null; + if (root.avatars.Count == 0) return null; + + // TODO: probable responsibility issues thus we might not want to affect the cache + // but avoids extra allocations in case the profile already exists + Profile profile = profileCache.Get(id) ?? new Profile(); + root.avatars[0].CopyTo(profile); + profileCache.Set(id, profile); + + return profile; + } + catch (UnityWebRequestException e) + { + if (e.ResponseCode == 404) + return null; + + throw; + } + } + + private class ProfileJsonRootDtoConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, GetProfileJsonRootDto? value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override GetProfileJsonRootDto? ReadJson(JsonReader reader, Type objectType, GetProfileJsonRootDto? existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var jObject = JObject.Load(reader); + existingValue ??= GetProfileJsonRootDto.Create(); + DeserializeProfileList(jObject["avatars"], ref existingValue.avatars); + return existingValue; + } + + private void DeserializeProfileList(JToken? root, ref List? list) + { + if (root is { Type: JTokenType.Array }) + { + list ??= new List(); + list.Clear(); + + foreach (JToken? item in root.Children()) + list.Add(DeserializeProfile(item, ProfileJsonDto.Create())); + } + else + list?.Clear(); + } + + private ProfileJsonDto DeserializeProfile(JToken? jObject, ProfileJsonDto profile) + { + if (jObject == null) return profile; + + profile.hasClaimedName = jObject["hasClaimedName"]?.Value() ?? false; + profile.description = jObject["description"]?.Value() ?? ""; + profile.tutorialStep = jObject["tutorialStep"]?.Value() ?? 0; + profile.name = jObject["name"]?.Value() ?? ""; + profile.userId = jObject["userId"]?.Value() ?? ""; + profile.email = jObject["email"]?.Value() ?? ""; + profile.ethAddress = jObject["ethAddress"]?.Value() ?? ""; + profile.version = jObject["version"]?.Value() ?? 0; + profile.unclaimedName = jObject["unclaimedName"]?.Value() ?? ""; + profile.hasConnectedWeb3 = jObject["hasConnectedWeb3"]?.Value() ?? false; + profile.avatar = DeserializeAvatar(jObject["avatar"], profile.avatar); + DeserializeArrayToList(jObject["blocked"], ref profile.blocked); + DeserializeArrayToList(jObject["interests"], ref profile.interests); + + return profile; + } + + private AvatarJsonDto DeserializeAvatar(JToken? jObject, AvatarJsonDto? avatar) + { + avatar ??= new AvatarJsonDto(); + avatar.eyes ??= new EyesJsonDto(); + avatar.eyes.color = DeserializeColor(jObject["eyes"]?["color"], new AvatarColorJsonDto()); + avatar.hair ??= new HairJsonDto(); + avatar.hair.color = DeserializeColor(jObject["hair"]?["color"], new AvatarColorJsonDto()); + avatar.skin ??= new SkinJsonDto(); + avatar.skin.color = DeserializeColor(jObject["skin"]?["color"], new AvatarColorJsonDto()); + + avatar.bodyShape = jObject["bodyShape"]?.Value() ?? ""; + + DeserializeArrayToList(jObject["wearables"], ref avatar.wearables); + DeserializeEmoteList(jObject["emotes"], ref avatar.emotes); + + avatar.snapshots ??= new AvatarSnapshotJsonDto(); + avatar.snapshots.face256 = jObject["snapshots"]?["face256"]?.Value() ?? ""; + avatar.snapshots.body = jObject["snapshots"]?["body"]?.Value() ?? ""; + + DeserializeArrayToList(jObject["forceRender"], ref avatar.forceRender); + + return avatar; + } + + private void DeserializeEmoteList(JToken? root, ref List? list) + { + if (root is { Type: JTokenType.Array }) + { + list ??= new List(); + list.Clear(); + + foreach (JToken? item in root.Children()) + list.Add(DeserializeEmote(item, new EmoteJsonDto())); + } + else + list?.Clear(); + } + + private EmoteJsonDto DeserializeEmote(JToken item, EmoteJsonDto emote) + { + emote.slot = item["slot"]?.Value() ?? 0; + emote.urn = item["urn"]?.Value() ?? ""; + return emote; + } + + private AvatarColorJsonDto? DeserializeColor(JToken? jObject, AvatarColorJsonDto color) + { + if (jObject == null) return null; + + color.r = jObject["r"]?.Value() ?? 0; + color.g = jObject["g"]?.Value() ?? 0; + color.b = jObject["b"]?.Value() ?? 0; + color.a = jObject["a"]?.Value() ?? 0; + + return color; + } + + private void DeserializeArrayToList(JToken? token, ref List? list) + { + if (token is { Type: JTokenType.Array }) + { + list ??= new List(); + list.Clear(); + + foreach (JToken? item in token.Children()) + { + string? s = item.ToObject(); + + if (s != null) + list.Add(s); + } + } + else + list?.Clear(); + } + } + } +} diff --git a/Explorer/Assets/DCL/Profiles/RealmProfileRepository.cs.meta b/Explorer/Assets/DCL/Profiles/RealmProfileRepository.cs.meta new file mode 100644 index 0000000000..2359060e51 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/RealmProfileRepository.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a7944ae192db4f679e49a23fc1733573 +timeCreated: 1702066145 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/csc.rsp b/Explorer/Assets/DCL/Profiles/csc.rsp new file mode 100644 index 0000000000..dcc377f897 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/csc.rsp @@ -0,0 +1 @@ +-nullable:enable \ No newline at end of file diff --git a/Explorer/Assets/DCL/Profiles/csc.rsp.meta b/Explorer/Assets/DCL/Profiles/csc.rsp.meta new file mode 100644 index 0000000000..faecf27b89 --- /dev/null +++ b/Explorer/Assets/DCL/Profiles/csc.rsp.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 26cfbef85be442b5a2ac83b5aa81fc90 +timeCreated: 1702562833 \ No newline at end of file diff --git a/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs b/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs index af759a8df5..10c0ce4404 100644 --- a/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs +++ b/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs @@ -79,7 +79,7 @@ public void UpdateProfilingCounters() { #if UNITY_EDITOR || DEVELOPMENT_BUILD ProfilingCounters.WearablesAssetsInCatalogAmount.Value = ((WearableCatalog)wearableCatalog).WearableAssetsInCatalog; - ProfilingCounters.WearablesAssetsInCacheAmount.Value = wearableAssetsCache.WearablesAssesCount; + ProfilingCounters.WearablesAssetsInCacheAmount.Value = wearableAssetsCache.WearablesAssetsCount; #endif } } diff --git a/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Account.cs b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Account.cs new file mode 100644 index 0000000000..d75e6f74d0 --- /dev/null +++ b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Account.cs @@ -0,0 +1,18 @@ +namespace DCL.Web3Authentication +{ + public class FakeWeb3Account : IWeb3Account + { + public string Address { get; } + + public FakeWeb3Account(string publicAddress) + { + Address = publicAddress; + } + + public string Sign(string message) => + $"fakeSign:{message}"; + + public bool Verify(string message, string signature) => + signature.StartsWith("fakeSign:") && signature.EndsWith(message); + } +} diff --git a/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Account.cs.meta b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Account.cs.meta new file mode 100644 index 0000000000..4938335b0f --- /dev/null +++ b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Account.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 32ac1af1d80849ad838336f54805d565 +timeCreated: 1702338928 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs index b29ad60d0e..ca3b52c354 100644 --- a/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs @@ -6,23 +6,29 @@ namespace DCL.Web3Authentication { public class FakeWeb3Authenticator : IWeb3Authenticator { - public IWeb3Identity? Identity { get; private set; } + private readonly string ephemeralPublicAddress; + + public IWeb3Identity Identity { get; private set; } + + public FakeWeb3Authenticator(string ephemeralPublicAddress) + { + this.ephemeralPublicAddress = ephemeralPublicAddress; + } public async UniTask LoginAsync(CancellationToken cancellationToken) { - var signer = NethereumAccount.CreateRandom(); - var ephemeralAccount = NethereumAccount.CreateRandom(); + var ephemeralAccount = new FakeWeb3Account(ephemeralPublicAddress); DateTime expiration = DateTime.Now.AddMinutes(600); var ephemeralMessage = $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address}\nExpiration: {expiration:s}"; - string ephemeralSignature = signer.Sign(ephemeralMessage); + string ephemeralSignature = ephemeralAccount.Sign(ephemeralMessage); var authChain = AuthChain.Create(); authChain.Set(AuthLinkType.SIGNER, new AuthLink { type = AuthLinkType.SIGNER, - payload = signer.Address, + payload = ephemeralAccount.Address, signature = "", }); @@ -38,9 +44,7 @@ public async UniTask LoginAsync(CancellationToken cancellationTok return Identity; } - public async UniTask LogoutAsync(CancellationToken cancellationToken) - { + public async UniTask LogoutAsync(CancellationToken cancellationToken) => Identity = null; - } } } diff --git a/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs.meta b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs.meta index 73ac0c1ba9..3a636e58bb 100644 --- a/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs.meta +++ b/Explorer/Assets/DCL/Web3Authentication/FakeWeb3Authenticator.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: bda545b47df24830b57f320eb546cab7 -timeCreated: 1701893593 \ No newline at end of file +guid: 5d878acb211945a5a4da55f2ce4cc612 +timeCreated: 1702339287 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3Authentication/RandomGeneratedWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3Authentication/RandomGeneratedWeb3Authenticator.cs new file mode 100644 index 0000000000..5e83fc5a09 --- /dev/null +++ b/Explorer/Assets/DCL/Web3Authentication/RandomGeneratedWeb3Authenticator.cs @@ -0,0 +1,46 @@ +using Cysharp.Threading.Tasks; +using System; +using System.Threading; + +namespace DCL.Web3Authentication +{ + public class RandomGeneratedWeb3Authenticator : IWeb3Authenticator + { + public IWeb3Identity? Identity { get; private set; } + + public async UniTask LoginAsync(CancellationToken cancellationToken) + { + var signer = NethereumAccount.CreateRandom(); + var ephemeralAccount = NethereumAccount.CreateRandom(); + DateTime expiration = DateTime.Now.AddMinutes(600); + + var ephemeralMessage = $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address}\nExpiration: {expiration:s}"; + string ephemeralSignature = signer.Sign(ephemeralMessage); + + var authChain = AuthChain.Create(); + + authChain.Set(AuthLinkType.SIGNER, new AuthLink + { + type = AuthLinkType.SIGNER, + payload = signer.Address, + signature = "", + }); + + authChain.Set(AuthLinkType.ECDSA_EPHEMERAL, new AuthLink + { + type = AuthLinkType.ECDSA_EPHEMERAL, + payload = ephemeralMessage, + signature = ephemeralSignature, + }); + + Identity = new DecentralandIdentity(ephemeralAccount, expiration, authChain); + + return Identity; + } + + public async UniTask LogoutAsync(CancellationToken cancellationToken) + { + Identity = null; + } + } +} diff --git a/Explorer/Assets/DCL/Web3Authentication/RandomGeneratedWeb3Authenticator.cs.meta b/Explorer/Assets/DCL/Web3Authentication/RandomGeneratedWeb3Authenticator.cs.meta new file mode 100644 index 0000000000..73ac0c1ba9 --- /dev/null +++ b/Explorer/Assets/DCL/Web3Authentication/RandomGeneratedWeb3Authenticator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bda545b47df24830b57f320eb546cab7 +timeCreated: 1701893593 \ No newline at end of file diff --git a/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs b/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs index 190917b4da..d4715cebd5 100644 --- a/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs +++ b/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs @@ -95,6 +95,31 @@ public static async UniTask CreateFromJsonAsync(this TRequest ty finally { await SwitchToMainThreadAsync(threadFlags); } } + public static async UniTask CreateFromNewtonsoftJsonAsync(this TRequest typedWebRequest, + WRThreadFlags threadFlags = WRThreadFlags.SwitchToThreadPool | WRThreadFlags.SwitchBackToMainThread, + CreateExceptionOnParseFail createCustomExceptionOnFailure = null, + params JsonConverter[] converters) + where TRequest: ITypedWebRequest, IGenericDownloadHandlerRequest + { + UnityWebRequest webRequest = typedWebRequest.UnityWebRequest; + string text = webRequest.downloadHandler.text; + + // Finalize the request immediately + webRequest.Dispose(); + + await SwitchToThreadAsync(threadFlags); + + try { return JsonConvert.DeserializeObject(text, converters); } + catch (Exception e) + { + if (createCustomExceptionOnFailure != null) + throw createCustomExceptionOnFailure(e, text); + else + throw; + } + finally { await SwitchToMainThreadAsync(threadFlags); } + } + /// /// Get data array from UnityWebRequest.downloadHandler.data without modifying the original array /// and finalize the request diff --git a/Explorer/Assets/Scripts/CommunicationData/URLHelpers/URNExtensions.cs b/Explorer/Assets/Scripts/CommunicationData/URLHelpers/URNExtensions.cs new file mode 100644 index 0000000000..8399b3021d --- /dev/null +++ b/Explorer/Assets/Scripts/CommunicationData/URLHelpers/URNExtensions.cs @@ -0,0 +1,19 @@ +namespace CommunicationData.URLHelpers +{ + public static class URNExtensions + { + // TODO: would be ideal that urns are type specific + public static string ShortenURN(this string input, int parts) + { + int index = -1; + + for (var i = 0; i < parts; i++) + { + index = input.IndexOf(':', index + 1); + if (index == -1) break; + } + + return index != -1 ? input[..index] : input; + } + } +} diff --git a/Explorer/Assets/Scripts/CommunicationData/URLHelpers/URNExtensions.cs.meta b/Explorer/Assets/Scripts/CommunicationData/URLHelpers/URNExtensions.cs.meta new file mode 100644 index 0000000000..dca9863ef7 --- /dev/null +++ b/Explorer/Assets/Scripts/CommunicationData/URLHelpers/URNExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 28d2c65bcca7464bac7c3addccd32833 +timeCreated: 1702477025 \ No newline at end of file diff --git a/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoader.cs b/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoader.cs index cf2fe1f399..9593a9c562 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoader.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoader.cs @@ -35,7 +35,6 @@ public class DynamicSceneLoader : MonoBehaviour private StaticContainer staticContainer; private DynamicWorldContainer dynamicWorldContainer; - private GlobalWorld globalWorld; private void Awake() @@ -72,9 +71,8 @@ private async UniTask InitializationFlowAsync(CancellationToken ct) { try { - // TODO: create the real web3 authenticator, missing decentralized app - var web3Authenticator = new FakeWeb3Authenticator(); - await web3Authenticator.LoginAsync(ct); + IWeb3Authenticator web3Authenticator = new RandomGeneratedWeb3Authenticator(); + IWeb3Identity web3Identity = await web3Authenticator.LoginAsync(ct); // First load the common global plugin bool isLoaded; @@ -123,16 +121,12 @@ void OnPluginInitialized((TPluginInterface plugin, bool succes return; } - globalWorld = dynamicWorldContainer.GlobalWorldFactory.Create(sceneSharedContainer.SceneFactory, dynamicWorldContainer.EmptyScenesWorldFactory, staticContainer.CharacterObject); + globalWorld = dynamicWorldContainer.GlobalWorldFactory.Create(sceneSharedContainer.SceneFactory, + dynamicWorldContainer.EmptyScenesWorldFactory, staticContainer.CharacterObject, web3Identity); dynamicWorldContainer.DebugContainer.Builder.Build(debugUiRoot); - void SetRealm(string selectedRealm) - { - ChangeRealmAsync(staticContainer, destroyCancellationToken, selectedRealm).Forget(); - } - - realmLauncher.OnRealmSelected += SetRealm; + realmLauncher.OnRealmSelected += ChangeRealm; } catch (OperationCanceledException) { @@ -146,19 +140,24 @@ void SetRealm(string selectedRealm) } } - private async UniTask ChangeRealmAsync(StaticContainer globalContainer, CancellationToken ct, string selectedRealm) + private void ChangeRealm(string selectedRealm) { - if (globalWorld != null) - await dynamicWorldContainer.RealmController.UnloadCurrentRealmAsync(globalWorld); + async UniTask ChangeRealmAsync(StaticContainer globalContainer, string selectedRealm, CancellationToken ct) + { + if (globalWorld != null) + await dynamicWorldContainer.RealmController.UnloadCurrentRealmAsync(globalWorld); + + await UniTask.SwitchToMainThread(); - await UniTask.SwitchToMainThread(); + Vector3 characterPos = ParcelMathHelper.GetPositionByParcelPosition(StartPosition); + characterPos.y = 1f; - Vector3 characterPos = ParcelMathHelper.GetPositionByParcelPosition(StartPosition); - characterPos.y = 1f; + globalContainer.CharacterObject.Controller.transform.position = characterPos; - globalContainer.CharacterObject.Controller.transform.position = characterPos; + await dynamicWorldContainer.RealmController.SetRealmAsync(globalWorld, URLDomain.FromString(selectedRealm), ct); + } - await dynamicWorldContainer.RealmController.SetRealmAsync(globalWorld, URLDomain.FromString(selectedRealm), ct); + ChangeRealmAsync(staticContainer, selectedRealm, CancellationToken.None).Forget(); } } } diff --git a/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs index f95923b80d..b40b1cabca 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs @@ -1,5 +1,6 @@ using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; +using DCL.AvatarRendering.AvatarShape.Systems; using DCL.AvatarRendering.Wearables; using DCL.DebugUtilities; using DCL.ParcelsService; @@ -7,6 +8,7 @@ using DCL.PluginSystem; using DCL.PluginSystem.Global; using DCL.Web3Authentication; +using DCL.Profiles; using DCL.WebRequests.Analytics; using ECS; using ECS.Prioritization.Components; @@ -39,6 +41,8 @@ public class DynamicWorldContainer : IDCLPlugin public IWeb3Authenticator Web3Authenticator { get; private set; } + public IProfileRepository ProfileRepository { get; private set; } + public void Dispose() { mvcManager.Dispose(); @@ -73,6 +77,9 @@ public void Dispose() MapRendererContainer mapRendererContainer = await MapRendererContainer.CreateAsync(staticContainer, dynamicSettings.MapRendererSettings, ct); var placesAPIService = new PlacesAPIService(new PlacesAPIClient(staticContainer.WebRequestsContainer.WebRequestController)); + container.ProfileRepository = new RealmProfileRepository(staticContainer.WebRequestsContainer.WebRequestController, realmData, + new DefaultProfileCache()); + var globalPlugins = new List { new CharacterMotionPlugin(staticContainer.AssetsProvisioner, staticContainer.CharacterObject, debugBuilder), @@ -83,6 +90,7 @@ public void Dispose() new WearablePlugin(staticContainer.AssetsProvisioner, staticContainer.WebRequestsContainer.WebRequestController, realmData, ASSET_BUNDLES_URL, staticContainer.CacheCleaner), new AvatarPlugin(staticContainer.ComponentsContainer.ComponentPoolsRegistry, staticContainer.AssetsProvisioner, staticContainer.SingletonSharedDependencies.FrameTimeBudgetProvider, staticContainer.SingletonSharedDependencies.MemoryBudgetProvider, realmData, debugBuilder, staticContainer.CacheCleaner), + new ProfilePlugin(container.ProfileRepository), new MapRendererPlugin(mapRendererContainer.MapRenderer), new MinimapPlugin(staticContainer.AssetsProvisioner, mvcManager, mapRendererContainer, placesAPIService), new ExplorePanelPlugin(staticContainer.AssetsProvisioner, mvcManager, mapRendererContainer, placesAPIService, parcelServiceContainer.TeleportController), @@ -99,7 +107,8 @@ public void Dispose() sceneLoadRadius, staticLoadPositions, realmData); container.GlobalWorldFactory = new GlobalWorldFactory(in staticContainer, staticContainer.RealmPartitionSettings, - exposedGlobalDataContainer.CameraSamplingData, realmSamplingData, ASSET_BUNDLES_URL, realmData, globalPlugins); + exposedGlobalDataContainer.CameraSamplingData, realmSamplingData, ASSET_BUNDLES_URL, realmData, globalPlugins, + debugBuilder); container.GlobalPlugins = globalPlugins; container.EmptyScenesWorldFactory = new EmptyScenesWorldFactory(staticContainer.SingletonSharedDependencies, staticContainer.ECSWorldPlugins); diff --git a/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs b/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs index 5a7d43c6b4..8e38637e92 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs @@ -3,19 +3,22 @@ using CommunicationData.URLHelpers; using CRDT; using CrdtEcsBridge.Components; +using DCL.AvatarRendering.AvatarShape.Systems; using DCL.AvatarRendering.Wearables; using DCL.AvatarRendering.Wearables.Helpers; using DCL.Character; using DCL.Character.Components; +using DCL.DebugUtilities; using DCL.ECSComponents; using DCL.GlobalPartitioning; using DCL.Optimization.PerformanceBudgeting; using DCL.Optimization.Pools; using DCL.PluginSystem.Global; using DCL.Systems; -using DCL.WebRequests; using DCL.Time; using DCL.Time.Systems; +using DCL.Web3Authentication; +using DCL.WebRequests; using ECS; using ECS.Groups; using ECS.LifeCycle; @@ -64,12 +67,15 @@ public class GlobalWorldFactory private readonly PhysicsTickProvider physicsTickProvider; private readonly IWebRequestController webRequestController; private readonly IReadOnlyList globalPlugins; + private readonly IDebugContainerBuilder debugContainerBuilder; private readonly IConcurrentBudgetProvider memoryBudgetProvider; private readonly StaticSettings staticSettings; - public GlobalWorldFactory(in StaticContainer staticContainer, IRealmPartitionSettings realmPartitionSettings, + public GlobalWorldFactory(in StaticContainer staticContainer, + IRealmPartitionSettings realmPartitionSettings, CameraSamplingData cameraSamplingData, RealmSamplingData realmSamplingData, - URLDomain assetBundlesURL, IRealmData realmData, IReadOnlyList globalPlugins) + URLDomain assetBundlesURL, IRealmData realmData, IReadOnlyList globalPlugins, + IDebugContainerBuilder debugContainerBuilder) { partitionedWorldsAggregateFactory = staticContainer.SingletonSharedDependencies.AggregateFactory; componentPoolsRegistry = staticContainer.ComponentsContainer.ComponentPoolsRegistry; @@ -81,13 +87,15 @@ public GlobalWorldFactory(in StaticContainer staticContainer, IRealmPartitionSet this.realmSamplingData = realmSamplingData; this.assetBundlesURL = assetBundlesURL; this.globalPlugins = globalPlugins; + this.debugContainerBuilder = debugContainerBuilder; this.realmData = realmData; memoryBudgetProvider = staticContainer.SingletonSharedDependencies.MemoryBudgetProvider; physicsTickProvider = staticContainer.PhysicsTickProvider; } - public GlobalWorld Create(ISceneFactory sceneFactory, IEmptyScenesWorldFactory emptyScenesWorldFactory, ICharacterObject characterObject) + public GlobalWorld Create(ISceneFactory sceneFactory, IEmptyScenesWorldFactory emptyScenesWorldFactory, ICharacterObject characterObject, + IWeb3Identity web3Identity) { var world = World.Create(); @@ -106,6 +114,7 @@ public GlobalWorld Create(ISceneFactory sceneFactory, IEmptyScenesWorldFactory e new TransformComponent { Transform = characterObject.Transform }, new PBAvatarShape { + Id = web3Identity.EphemeralAccount.Address, BodyShape = BodyShape.MALE, Wearables = { WearablesConstants.DefaultWearables.GetDefaultWearablesForBodyShape(BodyShape.MALE) }, Name = "Player", @@ -157,6 +166,8 @@ public GlobalWorld Create(ISceneFactory sceneFactory, IEmptyScenesWorldFactory e UpdatePhysicsTickSystem.InjectToWorld(ref builder, physicsTickProvider); UpdateTimeSystem.InjectToWorld(ref builder); + OwnAvatarLoaderFromDebugMenuSystem.InjectToWorld(ref builder, playerEntity, debugContainerBuilder, realmData); + var pluginArgs = new GlobalPluginArguments(playerEntity); foreach (IDCLGlobalPlugin plugin in globalPlugins) diff --git a/Explorer/Assets/Scripts/Global/Static/StaticSceneLauncher.cs b/Explorer/Assets/Scripts/Global/Static/StaticSceneLauncher.cs index 30a54325b9..047c6a62f6 100644 --- a/Explorer/Assets/Scripts/Global/Static/StaticSceneLauncher.cs +++ b/Explorer/Assets/Scripts/Global/Static/StaticSceneLauncher.cs @@ -35,7 +35,7 @@ public async UniTask InitializationFlowAsync(CancellationToken ct) { try { - var web3Authenticator = new FakeWeb3Authenticator(); + var web3Authenticator = new RandomGeneratedWeb3Authenticator(); await web3Authenticator.LoginAsync(ct); SceneSharedContainer sceneSharedContainer; diff --git a/Explorer/Assets/Scripts/Global/StaticContainer.cs b/Explorer/Assets/Scripts/Global/StaticContainer.cs index ba02502529..5d344c2da0 100644 --- a/Explorer/Assets/Scripts/Global/StaticContainer.cs +++ b/Explorer/Assets/Scripts/Global/StaticContainer.cs @@ -10,6 +10,7 @@ using DCL.PluginSystem.Global; using DCL.PluginSystem.World; using DCL.PluginSystem.World.Dependencies; +using DCL.Profiles; using DCL.Profiling; using DCL.ResourcesUnloading; using DCL.Time;