| | 1 | | using DCL; |
| | 2 | | using DCL.Helpers; |
| | 3 | | using DCL.UserProfiles; |
| | 4 | | using Decentraland.Renderer.KernelServices; |
| | 5 | | using System; |
| | 6 | | using System.Collections.Generic; |
| | 7 | | using System.Linq; |
| | 8 | | using UnityEditor; |
| | 9 | | using UnityEngine; |
| | 10 | | using Environment = DCL.Environment; |
| | 11 | |
|
| | 12 | | [CreateAssetMenu(fileName = "UserProfile", menuName = "UserProfile")] |
| | 13 | | public class UserProfile : ScriptableObject //TODO Move to base variable |
| | 14 | | { |
| | 15 | | public enum EmoteSource |
| | 16 | | { |
| | 17 | | EmotesWheel, |
| | 18 | | Shortcut, |
| | 19 | | Command, |
| | 20 | | Backpack, |
| | 21 | | EmoteLoop, |
| | 22 | | EmoteCancel, |
| | 23 | | } |
| | 24 | |
|
| | 25 | | private const string FALLBACK_NAME = "fallback"; |
| | 26 | |
|
| | 27 | | public event Action<UserProfile> OnUpdate; |
| | 28 | | public event Action<string, long, EmoteSource> OnAvatarEmoteSet; |
| | 29 | |
|
| 1016 | 30 | | public string userId => model.userId; |
| 240 | 31 | | public string ethAddress => model.ethAddress; |
| 304 | 32 | | public string userName => model.name; |
| 64 | 33 | | public string description => model.description; |
| 0 | 34 | | public string email => model.email; |
| 0 | 35 | | public string bodySnapshotURL => model.ComposeCorrectUrl(model.snapshots.body); |
| 130 | 36 | | public string face256SnapshotURL => model.ComposeCorrectUrl(model.snapshots.face256); |
| 0 | 37 | | public string baseUrl => model.baseUrl; |
| 0 | 38 | | public UserProfileModel.ParcelsWithAccess[] parcelsWithAccess => model.parcelsWithAccess; |
| 165 | 39 | | public List<string> blocked => model.blocked ?? new List<string>(); |
| 1 | 40 | | public List<string> muted => model.muted ?? new List<string>(); |
| 46 | 41 | | public bool hasConnectedWeb3 => model.hasConnectedWeb3; |
| 85 | 42 | | public bool hasClaimedName => model.hasClaimedName; |
| 170 | 43 | | public bool isGuest => !model.hasConnectedWeb3; |
| 174 | 44 | | public AvatarModel avatar => model.avatar; |
| 0 | 45 | | public int tutorialStep => model.tutorialStep; |
| 73 | 46 | | public List<UserProfileModel.Link> Links => model.links; |
| 62 | 47 | | public AdditionalInfo AdditionalInfo => model.AdditionalInfo; |
| | 48 | |
|
| 759 | 49 | | internal Dictionary<string, int> inventory = new (); |
| | 50 | |
|
| 759 | 51 | | public ILazyTextureObserver snapshotObserver = new LazyTextureObserver(); |
| 759 | 52 | | public ILazyTextureObserver bodySnapshotObserver = new LazyTextureObserver(); |
| | 53 | |
|
| | 54 | | internal static UserProfile ownUserProfile; |
| | 55 | |
|
| | 56 | | // Empty initialization to avoid null-checks |
| 759 | 57 | | internal UserProfileModel model = new () { avatar = new AvatarModel() }; |
| | 58 | |
|
| | 59 | | private UserProfileModel ModelFallback() => |
| 0 | 60 | | UserProfileModel.FallbackModel(FALLBACK_NAME, this.GetInstanceID()); |
| | 61 | |
|
| | 62 | | private AvatarModel AvatarFallback() => |
| 587 | 63 | | AvatarModel.FallbackModel(FALLBACK_NAME, this.GetInstanceID()); |
| | 64 | |
|
| 759 | 65 | | private int emoteLamportTimestamp = 1; |
| 1 | 66 | | private ClientEmotesKernelService emotes => Environment.i.serviceLocator.Get<IRPC>().Emotes(); |
| | 67 | |
|
| | 68 | | public void UpdateData(UserProfileModel newModel) |
| | 69 | | { |
| 720 | 70 | | if (newModel == null) |
| | 71 | | { |
| 0 | 72 | | if (!Application.isBatchMode) |
| 0 | 73 | | Debug.LogError("Model is null when updating UserProfile! Using fallback or previous model instead."); |
| | 74 | |
|
| | 75 | | // Check if there is a previous model to fallback to. Because default model has everything empty or null. |
| 0 | 76 | | newModel = string.IsNullOrEmpty(model.userId) ? ModelFallback() : model; |
| | 77 | | } |
| | 78 | |
|
| 720 | 79 | | if (newModel.avatar == null) |
| | 80 | | { |
| 662 | 81 | | model.avatar = new AvatarModel(); |
| | 82 | |
|
| 662 | 83 | | if (!Application.isBatchMode) |
| 0 | 84 | | Debug.LogError("Avatar is null when updating UserProfile! Using fallback or previous avatar instead."); |
| | 85 | |
|
| | 86 | | // Check if there is a previous avatar to fallback to. |
| 662 | 87 | | newModel.avatar = string.IsNullOrEmpty(model.userId) ? AvatarFallback() : model.avatar; |
| | 88 | | } |
| | 89 | |
|
| 720 | 90 | | bool faceSnapshotDirty = model.snapshots.face256 != newModel.snapshots.face256; |
| 720 | 91 | | bool bodySnapshotDirty = model.snapshots.body != newModel.snapshots.body; |
| | 92 | |
|
| 720 | 93 | | model.userId = newModel.userId; |
| 720 | 94 | | model.ethAddress = newModel.ethAddress; |
| 720 | 95 | | model.parcelsWithAccess = newModel.parcelsWithAccess; |
| 720 | 96 | | model.tutorialStep = newModel.tutorialStep; |
| 720 | 97 | | model.hasClaimedName = newModel.hasClaimedName; |
| 720 | 98 | | model.name = newModel.name; |
| 720 | 99 | | model.email = newModel.email; |
| 720 | 100 | | model.description = newModel.description; |
| 720 | 101 | | model.baseUrl = newModel.baseUrl; |
| 720 | 102 | | model.avatar.CopyFrom(newModel.avatar); |
| 720 | 103 | | model.snapshots = newModel.snapshots; |
| 720 | 104 | | model.hasConnectedWeb3 = newModel.hasConnectedWeb3; |
| 720 | 105 | | model.blocked = newModel.blocked; |
| 720 | 106 | | model.muted = newModel.muted; |
| 720 | 107 | | model.version = newModel.version; |
| 720 | 108 | | model.links = newModel.links; |
| 720 | 109 | | model.AdditionalInfo.CopyFrom(newModel.AdditionalInfo); |
| | 110 | |
|
| 720 | 111 | | if (faceSnapshotDirty) |
| 52 | 112 | | snapshotObserver.RefreshWithUri(face256SnapshotURL); |
| | 113 | |
|
| 720 | 114 | | if (bodySnapshotDirty) |
| 0 | 115 | | bodySnapshotObserver.RefreshWithUri(bodySnapshotURL); |
| | 116 | |
|
| 720 | 117 | | OnUpdate?.Invoke(this); |
| 25 | 118 | | } |
| | 119 | |
|
| | 120 | | public int GetItemAmount(string itemId) |
| | 121 | | { |
| 2 | 122 | | if (inventory == null || !inventory.ContainsKey(itemId)) |
| 0 | 123 | | return 0; |
| | 124 | |
|
| 2 | 125 | | return inventory[itemId]; |
| | 126 | | } |
| | 127 | |
|
| | 128 | | public void OverrideAvatar(AvatarModel newModel, Texture2D newFaceSnapshot) |
| | 129 | | { |
| 6 | 130 | | model.avatar.CopyFrom(newModel); |
| 6 | 131 | | this.snapshotObserver.RefreshWithTexture(newFaceSnapshot); |
| | 132 | |
|
| 6 | 133 | | OnUpdate?.Invoke(this); |
| 6 | 134 | | } |
| | 135 | |
|
| | 136 | | public void SetAvatarExpression(string id, EmoteSource source, bool rpcOnly = false) |
| | 137 | | { |
| 1 | 138 | | int timestamp = emoteLamportTimestamp++; |
| 1 | 139 | | avatar.expressionTriggerId = id; |
| 1 | 140 | | avatar.expressionTriggerTimestamp = timestamp; |
| | 141 | |
|
| | 142 | | // TODO: fix message `Timestamp` should NOT be `float`, we should use `int lamportTimestamp` or `long timeStamp` |
| 1 | 143 | | emotes?.TriggerExpression(new TriggerExpressionRequest() |
| | 144 | | { |
| | 145 | | Id = id, |
| | 146 | | Timestamp = rpcOnly ? -1 : timestamp |
| | 147 | | }); |
| | 148 | |
|
| 1 | 149 | | if (!rpcOnly) |
| | 150 | | { |
| 1 | 151 | | OnUpdate?.Invoke(this); |
| 1 | 152 | | OnAvatarEmoteSet?.Invoke(id, timestamp, source); |
| | 153 | | } |
| 1 | 154 | | } |
| | 155 | |
|
| | 156 | | public void SetInventory(IEnumerable<string> inventoryIds) |
| | 157 | | { |
| 173 | 158 | | inventory.Clear(); |
| 3240 | 159 | | inventory = inventoryIds.GroupBy(x => x).ToDictionary(x => x.Key, x => x.Count()); |
| 173 | 160 | | } |
| | 161 | |
|
| | 162 | | public void AddToInventory(string wearableId) |
| | 163 | | { |
| 0 | 164 | | if (inventory.ContainsKey(wearableId)) |
| 0 | 165 | | inventory[wearableId]++; |
| | 166 | | else |
| 0 | 167 | | inventory.Add(wearableId, 1); |
| 0 | 168 | | } |
| | 169 | |
|
| 0 | 170 | | public void RemoveFromInventory(string wearableId) { inventory.Remove(wearableId); } |
| | 171 | |
|
| 0 | 172 | | public bool ContainsInInventory(string wearableId) => inventory.ContainsKey(wearableId); |
| | 173 | |
|
| | 174 | | public string[] GetInventoryItemsIds() => |
| 0 | 175 | | inventory.Keys.ToArray(); |
| | 176 | |
|
| | 177 | | // TODO: Remove this call. The own user profile should be accessed via IUserProfileBridge.GetOwn() |
| | 178 | | public static UserProfile GetOwnUserProfile() |
| | 179 | | { |
| 3332 | 180 | | if (ownUserProfile == null) |
| 18 | 181 | | ownUserProfile = Resources.Load<UserProfile>("ScriptableObjects/OwnUserProfile"); |
| | 182 | |
|
| 3332 | 183 | | return ownUserProfile; |
| | 184 | | } |
| | 185 | |
|
| 0 | 186 | | public UserProfileModel CloneModel() => model.Clone(); |
| | 187 | |
|
| | 188 | | public bool IsBlocked(string userId) => |
| 65 | 189 | | blocked != null && blocked.Contains(userId); |
| | 190 | |
|
| | 191 | | public void Block(string userId) |
| | 192 | | { |
| 19 | 193 | | if (IsBlocked(userId)) |
| 0 | 194 | | return; |
| 19 | 195 | | blocked.Add(userId); |
| 19 | 196 | | OnUpdate?.Invoke(this); |
| 0 | 197 | | } |
| | 198 | |
|
| | 199 | | public void Unblock(string userId) |
| | 200 | | { |
| 0 | 201 | | if (!IsBlocked(userId)) |
| 0 | 202 | | return; |
| 0 | 203 | | blocked.Remove(userId); |
| 0 | 204 | | OnUpdate?.Invoke(this); |
| 0 | 205 | | } |
| | 206 | |
|
| 0 | 207 | | public bool HasEquipped(string wearableId) => avatar.wearables.Contains(wearableId); |
| | 208 | |
|
| | 209 | | #if UNITY_EDITOR |
| | 210 | | private void OnEnable() |
| | 211 | | { |
| 759 | 212 | | Application.quitting -= CleanUp; |
| 759 | 213 | | Application.quitting += CleanUp; |
| 759 | 214 | | } |
| | 215 | |
|
| | 216 | | private void CleanUp() |
| | 217 | | { |
| 0 | 218 | | Application.quitting -= CleanUp; |
| 0 | 219 | | if (AssetDatabase.Contains(this)) |
| 0 | 220 | | Resources.UnloadAsset(this); |
| 0 | 221 | | } |
| | 222 | | #endif |
| | 223 | | } |