| | 1 | | using Cysharp.Threading.Tasks; |
| | 2 | | using DCL.Helpers; |
| | 3 | | using DCL.Tasks; |
| | 4 | | using DCLServices.DCLFileBrowser; |
| | 5 | | using DCLServices.WearablesCatalogService; |
| | 6 | | using MainScripts.DCL.Components.Avatar.VRMExporter; |
| | 7 | | using MainScripts.DCL.Controllers.HUD.CharacterPreview; |
| | 8 | | using System; |
| | 9 | | using System.Collections.Generic; |
| | 10 | | using System.Linq; |
| | 11 | | using System.Text; |
| | 12 | | using System.Threading; |
| | 13 | | using UnityEngine; |
| | 14 | |
|
| | 15 | | namespace DCL.Backpack |
| | 16 | | { |
| | 17 | | public class BackpackEditorHUDController |
| | 18 | | { |
| | 19 | | private const string NEW_TOS_AND_EMAIL_SUBSCRIPTION_FF = "new_terms_of_service_and_email_subscription"; |
| | 20 | | private const string EMOTE_ID = "wave"; |
| | 21 | | private const string NOT_LOADED = "NOT_LOADED"; |
| | 22 | |
|
| | 23 | | private readonly IBackpackEditorHUDView view; |
| | 24 | | private readonly DataStore dataStore; |
| | 25 | | private readonly IWearablesCatalogService wearablesCatalogService; |
| | 26 | | private readonly IBackpackEmotesSectionController backpackEmotesSectionController; |
| | 27 | | private readonly IBackpackAnalyticsService backpackAnalyticsService; |
| | 28 | | private readonly IUserProfileBridge userProfileBridge; |
| | 29 | | private readonly RendererState rendererState; |
| | 30 | | private readonly WearableGridController wearableGridController; |
| | 31 | | private readonly AvatarSlotsHUDController avatarSlotsHUDController; |
| | 32 | | private readonly OutfitsController outfitsController; |
| | 33 | | private readonly IVRMExporter vrmExporter; |
| | 34 | | private readonly VRMDetailsController vrmDetailsController; |
| | 35 | | private readonly IDCLFileBrowserService fileBrowser; |
| | 36 | | private readonly IEmotesCatalogService emotesCatalogService; |
| 25 | 37 | | private readonly Dictionary<string, string> extendedWearableUrns = new (); |
| 25 | 38 | | private readonly Dictionary<string, Dictionary<string, string>> fallbackWearables = new () |
| | 39 | | { |
| | 40 | | {"urn:decentraland:off-chain:base-avatars:BaseFemale", new Dictionary<string, string> |
| | 41 | | { |
| | 42 | | {WearableLiterals.Categories.UPPER_BODY, "urn:decentraland:off-chain:base-avatars:white_top"}, |
| | 43 | | {WearableLiterals.Categories.LOWER_BODY, "urn:decentraland:off-chain:base-avatars:f_jeans"}, |
| | 44 | | {WearableLiterals.Categories.FEET, "urn:decentraland:off-chain:base-avatars:ruby_blue_loafer"}, |
| | 45 | | {WearableLiterals.Categories.HAIR, "urn:decentraland:off-chain:base-avatars:pony_tail"}, |
| | 46 | | {WearableLiterals.Categories.MOUTH, "urn:decentraland:off-chain:base-avatars:f_mouth_05"}, |
| | 47 | | {WearableLiterals.Categories.EYEBROWS, "urn:decentraland:off-chain:base-avatars:f_eyebrows_02"}, |
| | 48 | | {WearableLiterals.Categories.EYES, "urn:decentraland:off-chain:base-avatars:f_eyes_06"}, |
| | 49 | | }}, |
| | 50 | | {"urn:decentraland:off-chain:base-avatars:BaseMale", new Dictionary<string, string> |
| | 51 | | { |
| | 52 | | {WearableLiterals.Categories.UPPER_BODY, "urn:decentraland:off-chain:base-avatars:m_sweater_02"}, |
| | 53 | | {WearableLiterals.Categories.LOWER_BODY, "urn:decentraland:off-chain:base-avatars:soccer_pants"}, |
| | 54 | | {WearableLiterals.Categories.FEET, "urn:decentraland:off-chain:base-avatars:sport_colored_shoes"}, |
| | 55 | | {WearableLiterals.Categories.HAIR, "urn:decentraland:off-chain:base-avatars:cool_hair"}, |
| | 56 | | {WearableLiterals.Categories.FACIAL_HAIR, "urn:decentraland:off-chain:base-avatars:beard"}, |
| | 57 | | {WearableLiterals.Categories.EYEBROWS, "urn:decentraland:off-chain:base-avatars:eyebrows_00"}, |
| | 58 | | {WearableLiterals.Categories.EYES, "urn:decentraland:off-chain:base-avatars:eyes_00"}, |
| | 59 | | }} |
| | 60 | | }; |
| 25 | 61 | | private readonly BackpackEditorHUDModel model = new (); |
| 25 | 62 | | private readonly Dictionary<string, bool> vrmBlockingWearables = new (); // Key: id, Value: canBeUnEquipped |
| | 63 | |
|
| | 64 | | private string currentSlotSelected; |
| | 65 | | private bool avatarIsDirty; |
| 25 | 66 | | private CancellationTokenSource loadProfileCancellationToken = new (); |
| 25 | 67 | | private CancellationTokenSource setVisibilityCancellationToken = new (); |
| 25 | 68 | | private CancellationTokenSource outfitLoadCancellationToken = new (); |
| 25 | 69 | | private CancellationTokenSource saveCancellationToken = new (); |
| | 70 | | private string categoryPendingToPlayEmote; |
| | 71 | | private bool isTakingSnapshot; |
| | 72 | |
|
| 14 | 73 | | private bool isNewTermsOfServiceAndEmailSubscriptionEnabled => dataStore.featureFlags.flags.Get().IsFeatureEnabl |
| | 74 | |
|
| 29 | 75 | | private BaseCollection<string> previewEquippedWearables => dataStore.backpackV2.previewEquippedWearables; |
| | 76 | |
|
| 102 | 77 | | private UserProfile ownUserProfile => userProfileBridge.GetOwn(); |
| | 78 | |
|
| | 79 | | private int currentAnimationIndexShown; |
| 25 | 80 | | private bool shouldRequestOutfits = true; |
| | 81 | |
|
| | 82 | | private CancellationTokenSource vrmExportCts; |
| | 83 | |
|
| 25 | 84 | | public BackpackEditorHUDController( |
| | 85 | | IBackpackEditorHUDView view, |
| | 86 | | DataStore dataStore, |
| | 87 | | RendererState rendererState, |
| | 88 | | IUserProfileBridge userProfileBridge, |
| | 89 | | IWearablesCatalogService wearablesCatalogService, |
| | 90 | | IBackpackEmotesSectionController backpackEmotesSectionController, |
| | 91 | | IBackpackAnalyticsService backpackAnalyticsService, |
| | 92 | | WearableGridController wearableGridController, |
| | 93 | | AvatarSlotsHUDController avatarSlotsHUDController, |
| | 94 | | OutfitsController outfitsController, |
| | 95 | | IVRMExporter vrmExporter, |
| | 96 | | VRMDetailsController vrmDetailsController, |
| | 97 | | IDCLFileBrowserService fileBrowser, |
| | 98 | | IEmotesCatalogService emotesCatalogService) |
| | 99 | | { |
| 25 | 100 | | this.view = view; |
| 25 | 101 | | this.dataStore = dataStore; |
| 25 | 102 | | this.rendererState = rendererState; |
| 25 | 103 | | this.userProfileBridge = userProfileBridge; |
| 25 | 104 | | this.wearablesCatalogService = wearablesCatalogService; |
| 25 | 105 | | this.backpackEmotesSectionController = backpackEmotesSectionController; |
| 25 | 106 | | this.backpackAnalyticsService = backpackAnalyticsService; |
| 25 | 107 | | this.wearableGridController = wearableGridController; |
| 25 | 108 | | this.avatarSlotsHUDController = avatarSlotsHUDController; |
| 25 | 109 | | this.outfitsController = outfitsController; |
| 25 | 110 | | this.vrmExporter = vrmExporter; |
| 25 | 111 | | this.vrmDetailsController = vrmDetailsController; |
| 25 | 112 | | this.fileBrowser = fileBrowser; |
| 25 | 113 | | this.emotesCatalogService = emotesCatalogService; |
| | 114 | |
|
| 25 | 115 | | avatarSlotsHUDController.GenerateSlots(); |
| 25 | 116 | | ownUserProfile.OnUpdate += LoadUserProfileFromProfileUpdate; |
| 25 | 117 | | dataStore.HUDs.avatarEditorVisible.OnChange += OnBackpackVisibleChanged; |
| 25 | 118 | | dataStore.HUDs.isAvatarEditorInitialized.Set(true); |
| 25 | 119 | | dataStore.exploreV2.configureBackpackInFullscreenMenu.OnChange += ConfigureBackpackInFullscreenMenuChanged; |
| 25 | 120 | | dataStore.backpackV2.isWaitingToBeSavedAfterSignUp.OnChange += SaveBackpackBeforeSignUpFinishes; |
| | 121 | |
|
| 25 | 122 | | ConfigureBackpackInFullscreenMenuChanged(dataStore.exploreV2.configureBackpackInFullscreenMenu.Get(), null); |
| | 123 | |
|
| 25 | 124 | | backpackEmotesSectionController.OnNewEmoteAdded += OnNewEmoteAdded; |
| 25 | 125 | | backpackEmotesSectionController.OnEmotePreviewed += OnEmotePreviewed; |
| 25 | 126 | | backpackEmotesSectionController.OnEmoteEquipped += OnEmoteEquipped; |
| 25 | 127 | | backpackEmotesSectionController.OnEmoteUnEquipped += OnEmoteUnEquipped; |
| | 128 | |
|
| 25 | 129 | | wearableGridController.OnWearableSelected += SelectWearable; |
| 25 | 130 | | wearableGridController.OnWearableEquipped += EquipWearableFromGrid; |
| 25 | 131 | | wearableGridController.OnWearableUnequipped += UnEquipWearableFromGrid; |
| 25 | 132 | | wearableGridController.OnCategoryFilterRemoved += OnCategoryFilterRemoved; |
| | 133 | |
|
| 25 | 134 | | avatarSlotsHUDController.OnToggleSlot += ToggleSlot; |
| 25 | 135 | | avatarSlotsHUDController.OnUnequipFromSlot += UnEquipWearableFromSlot; |
| 25 | 136 | | avatarSlotsHUDController.OnHideUnhidePressed += UpdateOverrideHides; |
| | 137 | |
|
| 25 | 138 | | view.SetColorPickerVisibility(false); |
| 25 | 139 | | view.OnContinueSignup += SaveAvatarAndContinueSignupProcess; |
| 25 | 140 | | view.OnColorChanged += OnWearableColorChanged; |
| 25 | 141 | | view.OnColorPickerToggle += OnColorPickerToggled; |
| 25 | 142 | | view.OnAvatarUpdated += OnAvatarUpdated; |
| 25 | 143 | | view.OnOutfitsOpened += OnOutfitsOpened; |
| 25 | 144 | | view.OnSignUpBackClicked += OnSignUpBack; |
| 25 | 145 | | outfitsController.OnOutfitEquipped += OnOutfitEquipped; |
| | 146 | |
|
| 25 | 147 | | view.SetOutfitsEnabled(dataStore.featureFlags.flags.Get().IsFeatureEnabled("outfits")); |
| 25 | 148 | | SetVisibility(dataStore.HUDs.avatarEditorVisible.Get(), saveAvatar: false); |
| | 149 | |
|
| 25 | 150 | | bool vrmFeatureEnabled = this.dataStore.featureFlags.flags.Get().IsFeatureEnabled("vrm_export"); |
| 25 | 151 | | view.OnVRMExport += OnVrmExport; |
| 25 | 152 | | view.OnVRMDetailsOpened += OnVRMDetailsOpened; |
| 25 | 153 | | view.OnVRMDetailsClosed += UpdateVRMExportWarning; |
| 25 | 154 | | view.SetVRMButtonActive(vrmFeatureEnabled); |
| 25 | 155 | | view.SetVRMButtonEnabled(vrmFeatureEnabled); |
| 25 | 156 | | view.SetVRMSuccessToastActive(false); |
| | 157 | |
|
| 25 | 158 | | vrmDetailsController.OnWearableUnequipped += UnEquipWearableFromGrid; |
| 25 | 159 | | vrmDetailsController.OnWearableEquipped += EquipWearableFromGrid; |
| 25 | 160 | | UpdateVRMExportWarning(); |
| 25 | 161 | | } |
| | 162 | |
|
| | 163 | | private void OnVRMDetailsOpened() |
| | 164 | | { |
| 0 | 165 | | vrmDetailsController.Initialize(vrmBlockingWearables); |
| 0 | 166 | | } |
| | 167 | |
|
| | 168 | | private void OnOutfitEquipped(OutfitItem outfit) |
| | 169 | | { |
| 0 | 170 | | Dictionary<string, WearableItem> keyValuePairs = new Dictionary<string, WearableItem>(model.wearables); |
| | 171 | |
|
| 0 | 172 | | foreach (KeyValuePair<string, WearableItem> keyValuePair in keyValuePairs) |
| 0 | 173 | | UnEquipWearable(keyValuePair.Key, UnequipWearableSource.None, false, false); |
| | 174 | |
|
| 0 | 175 | | foreach (string forcedCategory in model.forceRender) |
| 0 | 176 | | UpdateOverrideHides(forcedCategory, false, false); |
| | 177 | |
|
| 0 | 178 | | outfitLoadCancellationToken = new CancellationTokenSource(); |
| 0 | 179 | | LoadAndEquipOutfitWearables(outfit, outfitLoadCancellationToken.Token).Forget(); |
| 0 | 180 | | } |
| | 181 | |
|
| | 182 | | private async UniTaskVoid LoadAndEquipOutfitWearables(OutfitItem outfit, CancellationToken cancellationToken) |
| | 183 | | { |
| 0 | 184 | | if (!wearablesCatalogService.WearablesCatalog.ContainsKey(outfit.outfit.bodyShape)) |
| 0 | 185 | | await wearablesCatalogService.RequestWearableAsync(outfit.outfit.bodyShape, cancellationToken); |
| | 186 | |
|
| 0 | 187 | | foreach (string outfitWearable in outfit.outfit.wearables) |
| | 188 | | { |
| 0 | 189 | | if (wearablesCatalogService.WearablesCatalog.ContainsKey(ExtendedUrnParser.GetShortenedUrn(outfitWearabl |
| | 190 | |
|
| 0 | 191 | | try { await wearablesCatalogService.RequestWearableAsync(outfitWearable, cancellationToken); } |
| 0 | 192 | | catch (Exception e) { Debug.LogWarning($"Cannot resolve the wearable {outfitWearable} for the outfit {ou |
| 0 | 193 | | } |
| | 194 | |
|
| 0 | 195 | | EquipWearable(outfit.outfit.bodyShape, EquipWearableSource.Outfit, setAsDirty: false, updateAvatarPreview: f |
| | 196 | |
|
| 0 | 197 | | foreach (string outfitWearable in outfit.outfit.wearables) |
| 0 | 198 | | EquipWearable(outfitWearable, EquipWearableSource.Outfit, setAsDirty: true, updateAvatarPreview: true); |
| | 199 | |
|
| 0 | 200 | | SetAllColors(outfit.outfit.eyes.color, outfit.outfit.hair.color, outfit.outfit.skin.color); |
| | 201 | |
|
| 0 | 202 | | foreach (string forcedCategory in outfit.outfit.forceRender) |
| 0 | 203 | | UpdateOverrideHides(forcedCategory, true, true); |
| 0 | 204 | | } |
| | 205 | |
|
| | 206 | | private void OnOutfitsOpened() |
| | 207 | | { |
| 0 | 208 | | if (!shouldRequestOutfits) return; |
| | 209 | |
|
| 0 | 210 | | outfitsController.RequestOwnedOutfits(); |
| 0 | 211 | | shouldRequestOutfits = false; |
| 0 | 212 | | } |
| | 213 | |
|
| | 214 | | public void Dispose() |
| | 215 | | { |
| 25 | 216 | | vrmExportCts?.SafeCancelAndDispose(); |
| 25 | 217 | | ownUserProfile.OnUpdate -= LoadUserProfileFromProfileUpdate; |
| 25 | 218 | | dataStore.HUDs.avatarEditorVisible.OnChange -= OnBackpackVisibleChanged; |
| 25 | 219 | | dataStore.exploreV2.configureBackpackInFullscreenMenu.OnChange -= ConfigureBackpackInFullscreenMenuChanged; |
| 25 | 220 | | dataStore.backpackV2.isWaitingToBeSavedAfterSignUp.OnChange -= SaveBackpackBeforeSignUpFinishes; |
| | 221 | |
|
| 25 | 222 | | backpackEmotesSectionController.OnNewEmoteAdded -= OnNewEmoteAdded; |
| 25 | 223 | | backpackEmotesSectionController.OnEmotePreviewed -= OnEmotePreviewed; |
| 25 | 224 | | backpackEmotesSectionController.OnEmoteEquipped -= OnEmoteEquipped; |
| 25 | 225 | | backpackEmotesSectionController.OnEmoteUnEquipped -= OnEmoteUnEquipped; |
| 25 | 226 | | backpackEmotesSectionController.Dispose(); |
| | 227 | |
|
| 25 | 228 | | wearableGridController.OnWearableSelected -= SelectWearable; |
| 25 | 229 | | wearableGridController.OnWearableEquipped -= EquipWearableFromGrid; |
| 25 | 230 | | wearableGridController.OnWearableUnequipped -= UnEquipWearableFromGrid; |
| 25 | 231 | | wearableGridController.OnCategoryFilterRemoved -= OnCategoryFilterRemoved; |
| 25 | 232 | | wearableGridController.Dispose(); |
| | 233 | |
|
| 25 | 234 | | avatarSlotsHUDController.OnToggleSlot -= ToggleSlot; |
| 25 | 235 | | avatarSlotsHUDController.OnUnequipFromSlot -= UnEquipWearableFromSlot; |
| 25 | 236 | | avatarSlotsHUDController.OnHideUnhidePressed -= UpdateOverrideHides; |
| 25 | 237 | | avatarSlotsHUDController.Dispose(); |
| | 238 | |
|
| 25 | 239 | | view.OnColorChanged -= OnWearableColorChanged; |
| 25 | 240 | | view.OnContinueSignup -= SaveAvatarAndContinueSignupProcess; |
| 25 | 241 | | view.OnColorPickerToggle -= OnColorPickerToggled; |
| 25 | 242 | | view.OnAvatarUpdated -= OnAvatarUpdated; |
| 25 | 243 | | view.OnOutfitsOpened -= OnOutfitsOpened; |
| 25 | 244 | | view.OnVRMExport -= OnVrmExport; |
| 25 | 245 | | view.OnSignUpBackClicked -= OnSignUpBack; |
| 25 | 246 | | outfitsController.OnOutfitEquipped -= OnOutfitEquipped; |
| 25 | 247 | | view.Dispose(); |
| | 248 | |
|
| 25 | 249 | | vrmBlockingWearables.Clear(); |
| 25 | 250 | | view.OnVRMDetailsOpened -= OnVRMDetailsOpened; |
| 25 | 251 | | view.OnVRMDetailsClosed -= UpdateVRMExportWarning; |
| 25 | 252 | | vrmDetailsController.OnWearableUnequipped -= UnEquipWearableFromGrid; |
| 25 | 253 | | vrmDetailsController.OnWearableEquipped -= EquipWearableFromGrid; |
| 25 | 254 | | } |
| | 255 | |
|
| | 256 | | private void UpdateOverrideHides(string category, bool toggleOn) => |
| 0 | 257 | | UpdateOverrideHides(category, toggleOn, true); |
| | 258 | |
|
| | 259 | | private void UpdateOverrideHides(string category, bool toggleOn, bool setAsDirty) |
| | 260 | | { |
| 5 | 261 | | if (toggleOn) |
| 0 | 262 | | model.forceRender.Add(category); |
| | 263 | | else |
| 5 | 264 | | model.forceRender.Remove(category); |
| | 265 | |
|
| 5 | 266 | | avatarIsDirty = setAsDirty; |
| 5 | 267 | | avatarSlotsHUDController.Recalculate(model.forceRender); |
| 5 | 268 | | UpdateAvatarPreview(); |
| 5 | 269 | | } |
| | 270 | |
|
| | 271 | | private void OnBackpackVisibleChanged(bool current, bool _) => |
| 13 | 272 | | SetVisibility(current, saveAvatar: avatarIsDirty); |
| | 273 | |
|
| | 274 | | private void SetVisibility(bool visible, bool saveAvatar = true) |
| | 275 | | { |
| | 276 | | async UniTaskVoid SetVisibilityAsync(bool visible, bool saveAvatar, CancellationToken cancellationToken) |
| | 277 | | { |
| 39 | 278 | | if (visible) |
| | 279 | | { |
| 9 | 280 | | avatarIsDirty = false; |
| 9 | 281 | | dataStore.skyboxConfig.avatarMatProfile.Set(AvatarMaterialProfile.InEditor); |
| 9 | 282 | | backpackEmotesSectionController.RestoreEmoteSlots(); |
| 9 | 283 | | backpackEmotesSectionController.LoadEmotes(); |
| 9 | 284 | | wearableGridController.LoadWearables(); |
| 9 | 285 | | wearableGridController.LoadCollections(); |
| 9 | 286 | | LoadUserProfile(ownUserProfile); |
| 9 | 287 | | view.Show(); |
| 9 | 288 | | view.SetSignUpModeActive(dataStore.common.isSignUpFlow.Get() && isNewTermsOfServiceAndEmailSubscript |
| | 289 | |
|
| 9 | 290 | | if (dataStore.common.isSignUpFlow.Get()) |
| | 291 | | { |
| 5 | 292 | | if (isNewTermsOfServiceAndEmailSubscriptionEnabled) |
| 2 | 293 | | view.HideContinueSignup(); |
| | 294 | | else |
| 3 | 295 | | view.ShowContinueSignup(); |
| | 296 | |
|
| 5 | 297 | | avatarSlotsHUDController.SelectSlot(WearableLiterals.Categories.BODY_SHAPE); |
| | 298 | | } |
| | 299 | | else |
| 4 | 300 | | view.HideContinueSignup(); |
| | 301 | | } |
| | 302 | | else |
| | 303 | | { |
| 30 | 304 | | if (saveAvatar) |
| 5 | 305 | | await SaveAsync(cancellationToken); |
| | 306 | | else |
| 25 | 307 | | CloseView(); |
| | 308 | |
|
| 30 | 309 | | wearableGridController.CancelWearableLoading(); |
| | 310 | | } |
| 39 | 311 | | } |
| | 312 | |
|
| 39 | 313 | | setVisibilityCancellationToken = setVisibilityCancellationToken.SafeRestart(); |
| 39 | 314 | | SetVisibilityAsync(visible, saveAvatar, setVisibilityCancellationToken.Token).Forget(); |
| 39 | 315 | | } |
| | 316 | |
|
| | 317 | | private void CloseView() |
| | 318 | | { |
| 30 | 319 | | if (isTakingSnapshot) |
| 0 | 320 | | return; |
| | 321 | |
|
| 30 | 322 | | view.Hide(); |
| 30 | 323 | | view.ResetPreviewPanel(); |
| 30 | 324 | | wearableGridController.ResetFilters(); |
| 30 | 325 | | dataStore.skyboxConfig.avatarMatProfile.Set(AvatarMaterialProfile.InWorld); |
| 30 | 326 | | } |
| | 327 | |
|
| | 328 | | private void ConfigureBackpackInFullscreenMenuChanged(Transform currentParentTransform, Transform previousParent |
| 25 | 329 | | view.SetAsFullScreenMenuMode(currentParentTransform); |
| | 330 | |
|
| | 331 | | private void SaveBackpackBeforeSignUpFinishes(bool isBackpackWaitingToBeSaved, bool _) |
| | 332 | | { |
| 0 | 333 | | if (!isBackpackWaitingToBeSaved) |
| 0 | 334 | | return; |
| | 335 | |
|
| 0 | 336 | | view.SetSignUpStage(SignUpStage.CustomizeAvatar); |
| 0 | 337 | | saveCancellationToken = saveCancellationToken.SafeRestart(); |
| 0 | 338 | | SaveAsync(saveCancellationToken.Token).Forget(); |
| 0 | 339 | | } |
| | 340 | |
|
| | 341 | | private void LoadUserProfileFromProfileUpdate(UserProfile userProfile) |
| | 342 | | { |
| 7 | 343 | | bool isEditorVisible = rendererState.Get() && view.isVisible; |
| 13 | 344 | | if (!isEditorVisible) return; |
| 1 | 345 | | LoadUserProfile(userProfile); |
| 1 | 346 | | } |
| | 347 | |
|
| | 348 | | private void LoadUserProfile(UserProfile userProfile) |
| | 349 | | { |
| 10 | 350 | | if (avatarIsDirty) |
| | 351 | | { |
| 0 | 352 | | Debug.LogWarning("Skip the load of the user profile: avatarIsDirty=true"); |
| 0 | 353 | | return; |
| | 354 | | } |
| | 355 | |
|
| 10 | 356 | | if (userProfile == null) |
| | 357 | | { |
| 0 | 358 | | Debug.LogWarning("Skip the load of the user profile: userProfile=null"); |
| 0 | 359 | | return; |
| | 360 | | } |
| | 361 | |
|
| 10 | 362 | | if (userProfile.avatar == null || string.IsNullOrEmpty(userProfile.avatar.bodyShape)) |
| | 363 | | { |
| 8 | 364 | | Debug.LogWarning("Skip the load of the user profile: the avatar body shape is invalid"); |
| 8 | 365 | | return; |
| | 366 | | } |
| | 367 | |
|
| | 368 | | async UniTaskVoid LoadUserAvatarAsync(AvatarModel avatar, CancellationToken cancellationToken) |
| | 369 | | { |
| | 370 | | try |
| | 371 | | { |
| 4 | 372 | | foreach (AvatarModel.AvatarEmoteEntry emote in avatar.emotes) |
| | 373 | | { |
| 0 | 374 | | string shortenedEmotedUrn = ExtendedUrnParser.GetShortenedUrn(emote.urn); |
| 0 | 375 | | if (!emote.urn.Equals(shortenedEmotedUrn)) |
| 0 | 376 | | extendedWearableUrns[shortenedEmotedUrn] = emote.urn; |
| | 377 | | } |
| | 378 | |
|
| 2 | 379 | | wearablesCatalogService.WearablesCatalog.TryGetValue(avatar.bodyShape, out var bodyShape); |
| 2 | 380 | | bodyShape ??= await wearablesCatalogService.RequestWearableAsync(avatar.bodyShape, cancellationToken |
| | 381 | |
|
| 2 | 382 | | UnEquipCurrentBodyShape(false); |
| 2 | 383 | | EquipBodyShape(bodyShape, false); |
| | 384 | |
|
| 2 | 385 | | model.skinColor = avatar.skinColor; |
| 2 | 386 | | model.hairColor = avatar.hairColor; |
| 2 | 387 | | model.eyesColor = avatar.eyeColor; |
| 2 | 388 | | model.forceRender = new HashSet<string>(avatar.forceRender); |
| 2 | 389 | | model.wearables.Clear(); |
| 2 | 390 | | previewEquippedWearables.Clear(); |
| | 391 | |
|
| 2 | 392 | | int wearablesCount = avatar.wearables.Count; |
| | 393 | |
|
| 24 | 394 | | for (var i = 0; i < wearablesCount; i++) |
| | 395 | | { |
| 10 | 396 | | string wearableId = avatar.wearables[i]; |
| 10 | 397 | | string shortenedWearableId = ExtendedUrnParser.GetShortenedUrn(wearableId); |
| | 398 | |
|
| 10 | 399 | | if (!wearableId.Equals(shortenedWearableId)) |
| 0 | 400 | | extendedWearableUrns[shortenedWearableId] = wearableId; |
| | 401 | |
|
| 10 | 402 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(shortenedWearableId, out WearableItem |
| | 403 | | { |
| 0 | 404 | | try { wearable = await wearablesCatalogService.RequestWearableAsync(shortenedWearableId, can |
| 0 | 405 | | catch (OperationCanceledException) { throw; } |
| | 406 | | catch (Exception e) |
| | 407 | | { |
| 0 | 408 | | Debug.LogError($"Cannot load the wearable {shortenedWearableId}"); |
| 0 | 409 | | Debug.LogException(e); |
| 0 | 410 | | continue; |
| | 411 | | } |
| | 412 | | } |
| | 413 | |
|
| 20 | 414 | | try { EquipWearable(wearableId, wearable, EquipWearableSource.None, false, false, false); } |
| 0 | 415 | | catch (OperationCanceledException) { throw; } |
| | 416 | | catch (Exception e) |
| | 417 | | { |
| 0 | 418 | | Debug.LogError($"Cannot equip the wearable {shortenedWearableId}"); |
| 0 | 419 | | Debug.LogException(e); |
| 0 | 420 | | } |
| 10 | 421 | | } |
| | 422 | |
|
| 2 | 423 | | avatarSlotsHUDController.Recalculate(model.forceRender); |
| 2 | 424 | | UpdateAvatarModel(model.ToAvatarModel()); |
| 2 | 425 | | } |
| 0 | 426 | | catch (OperationCanceledException) { Debug.LogWarning("Skip the load of the user profile: the operation |
| 0 | 427 | | catch (Exception e) { Debug.LogException(e); } |
| 2 | 428 | | } |
| | 429 | |
|
| 2 | 430 | | loadProfileCancellationToken = loadProfileCancellationToken.SafeRestart(); |
| 2 | 431 | | LoadUserAvatarAsync(userProfile.avatar, loadProfileCancellationToken.Token).Forget(); |
| 2 | 432 | | } |
| | 433 | |
|
| | 434 | | private void UpdateAvatarPreview() |
| | 435 | | { |
| 6 | 436 | | AvatarModel modelToUpdate = model.ToAvatarModel(); |
| | 437 | |
|
| | 438 | | // We always keep the loaded emotes into the Avatar Preview |
| 12 | 439 | | foreach (string emoteId in dataStore.emotesCustomization.currentLoadedEmotes.Get()) |
| 0 | 440 | | modelToUpdate.emotes.Add(new AvatarModel.AvatarEmoteEntry |
| | 441 | | { urn = emoteId }); |
| | 442 | |
|
| 6 | 443 | | UpdateAvatarModel(modelToUpdate); |
| 6 | 444 | | } |
| | 445 | |
|
| | 446 | | private void OnNewEmoteAdded(string emoteId) => |
| 1 | 447 | | UpdateAvatarPreview(); |
| | 448 | |
|
| | 449 | | private void OnEmotePreviewed(string emoteId) => |
| 1 | 450 | | view.PlayPreviewEmote(emoteId); |
| | 451 | |
|
| | 452 | | private void OnEmoteEquipped(string emoteId) => |
| 0 | 453 | | avatarIsDirty = true; |
| | 454 | |
|
| | 455 | | private void OnEmoteUnEquipped(string emoteId) => |
| 0 | 456 | | avatarIsDirty = true; |
| | 457 | |
|
| | 458 | | private void EquipBodyShape(WearableItem bodyShape, bool setAsDirty = true) |
| | 459 | | { |
| 7 | 460 | | if (bodyShape.data.category != WearableLiterals.Categories.BODY_SHAPE) |
| | 461 | | { |
| 0 | 462 | | Debug.LogError($"Item ({bodyShape.id} is not a body shape"); |
| 0 | 463 | | return; |
| | 464 | | } |
| | 465 | |
|
| 7 | 466 | | model.bodyShape = bodyShape; |
| 7 | 467 | | dataStore.backpackV2.previewBodyShape.Set(bodyShape.id); |
| 7 | 468 | | avatarSlotsHUDController.Equip(bodyShape, bodyShape.id, model.forceRender); |
| 7 | 469 | | backpackEmotesSectionController.SetEquippedBodyShape(bodyShape.id); |
| 7 | 470 | | wearableGridController.Equip(bodyShape.id); |
| 7 | 471 | | wearableGridController.UpdateBodyShapeCompatibility(bodyShape.id); |
| | 472 | |
|
| 7 | 473 | | if (setAsDirty) |
| 5 | 474 | | avatarIsDirty = true; |
| 7 | 475 | | } |
| | 476 | |
|
| | 477 | | private void SaveAvatarAndContinueSignupProcess() |
| | 478 | | { |
| 1 | 479 | | if (!isNewTermsOfServiceAndEmailSubscriptionEnabled) |
| | 480 | | { |
| 1 | 481 | | SetVisibility(false, saveAvatar: true); |
| 1 | 482 | | return; |
| | 483 | | } |
| | 484 | |
|
| 0 | 485 | | dataStore.HUDs.signupVisible.Set(true); |
| 0 | 486 | | view.SetSignUpStage(SignUpStage.SetNameAndEmail); |
| 0 | 487 | | view.SetAvatarPreviewFocus(PreviewCameraFocus.FaceEditing); |
| 0 | 488 | | view.PlayPreviewEmote(EMOTE_ID); |
| 0 | 489 | | backpackAnalyticsService.SendAvatarEditSuccessNuxAnalytic(); |
| 0 | 490 | | } |
| | 491 | |
|
| | 492 | | private async UniTask SaveAsync(CancellationToken cancellationToken) |
| | 493 | | { |
| | 494 | | try |
| | 495 | | { |
| 5 | 496 | | await TakeSnapshotsAndSaveAvatarAsync(cancellationToken); |
| | 497 | |
|
| 5 | 498 | | if (dataStore.backpackV2.isWaitingToBeSavedAfterSignUp.Get()) |
| 0 | 499 | | dataStore.backpackV2.isWaitingToBeSavedAfterSignUp.Set(false); |
| | 500 | |
|
| 5 | 501 | | CloseView(); |
| 5 | 502 | | } |
| 0 | 503 | | catch (OperationCanceledException) { } |
| | 504 | | catch (Exception e) |
| | 505 | | { |
| 0 | 506 | | Debug.LogException(e); |
| 0 | 507 | | CloseView(); |
| 0 | 508 | | } |
| 5 | 509 | | } |
| | 510 | |
|
| | 511 | | private UniTask TakeSnapshotsAndSaveAvatarAsync(CancellationToken cancellationToken) |
| | 512 | | { |
| 5 | 513 | | UniTaskCompletionSource task = new (); |
| 5 | 514 | | isTakingSnapshot = true; |
| | 515 | |
|
| 5 | 516 | | TakeSnapshots( |
| | 517 | | onSuccess: (face256Snapshot, bodySnapshot) => |
| | 518 | | { |
| 6 | 519 | | cancellationToken.ThrowIfCancellationRequested(); |
| 6 | 520 | | SaveAvatar(face256Snapshot, bodySnapshot); |
| 6 | 521 | | isTakingSnapshot = false; |
| 6 | 522 | | task.TrySetResult(); |
| 6 | 523 | | }, |
| | 524 | | onFailed: () => |
| | 525 | | { |
| 0 | 526 | | cancellationToken.ThrowIfCancellationRequested(); |
| 0 | 527 | | SaveAvatar(new Texture2D(256, 256), new Texture2D(256, 256)); |
| 0 | 528 | | isTakingSnapshot = false; |
| 0 | 529 | | task.TrySetException(new Exception("Error taking avatar screenshots.")); |
| 0 | 530 | | }); |
| | 531 | |
|
| 5 | 532 | | return task.Task; |
| | 533 | | } |
| | 534 | |
|
| | 535 | | private void TakeSnapshots(IBackpackEditorHUDView.OnSnapshotsReady onSuccess, Action onFailed) |
| | 536 | | { |
| 5 | 537 | | view.TakeSnapshotsAfterStopPreviewAnimation( |
| 6 | 538 | | onSuccess: (face256Snapshot, bodySnapshot) => onSuccess?.Invoke(face256Snapshot, bodySnapshot), |
| 0 | 539 | | onFailed: () => onFailed?.Invoke()); |
| 5 | 540 | | } |
| | 541 | |
|
| | 542 | | private void SaveAvatar(Texture2D face256Snapshot, Texture2D bodySnapshot) |
| | 543 | | { |
| 6 | 544 | | var avatarModel = model.ToAvatarModel(); |
| | 545 | |
|
| | 546 | | // Restore extended urns |
| 34 | 547 | | for (var i = 0; i < avatarModel.wearables.Count; i++) |
| | 548 | | { |
| 11 | 549 | | string shortenedUrn = avatarModel.wearables[i]; |
| | 550 | |
|
| 11 | 551 | | if (extendedWearableUrns.TryGetValue(shortenedUrn, out string urn)) |
| 11 | 552 | | avatarModel.wearables[i] = urn; |
| | 553 | | } |
| | 554 | |
|
| | 555 | | // Add the equipped emotes to the avatar model |
| 6 | 556 | | List<AvatarModel.AvatarEmoteEntry> emoteEntries = new List<AvatarModel.AvatarEmoteEntry>(); |
| 6 | 557 | | int equippedEmotesCount = dataStore.emotesCustomization.unsavedEquippedEmotes.Count(); |
| | 558 | |
|
| 132 | 559 | | for (var i = 0; i < equippedEmotesCount; i++) |
| | 560 | | { |
| 60 | 561 | | var equippedEmote = dataStore.emotesCustomization.unsavedEquippedEmotes[i]; |
| | 562 | |
|
| 60 | 563 | | if (equippedEmote == null) |
| | 564 | | continue; |
| | 565 | |
|
| 0 | 566 | | string id = equippedEmote.id; |
| | 567 | |
|
| 0 | 568 | | if (extendedWearableUrns.TryGetValue(id, out string urn)) |
| 0 | 569 | | id = urn; |
| 0 | 570 | | else if (emotesCatalogService.TryGetOwnedUrn(id, out string extendedUrn)) |
| 0 | 571 | | if (!string.IsNullOrEmpty(extendedUrn)) |
| 0 | 572 | | id = extendedUrn; |
| | 573 | |
|
| 0 | 574 | | emoteEntries.Add(new AvatarModel.AvatarEmoteEntry { slot = i, urn = id }); |
| | 575 | | } |
| | 576 | |
|
| 6 | 577 | | avatarModel.emotes = emoteEntries; |
| | 578 | |
|
| 6 | 579 | | dataStore.emotesCustomization.equippedEmotes.Set(dataStore.emotesCustomization.unsavedEquippedEmotes.Get()); |
| | 580 | |
|
| 6 | 581 | | userProfileBridge.SendSaveAvatar(avatarModel, face256Snapshot, bodySnapshot, dataStore.common.isSignUpFlow.G |
| 6 | 582 | | ownUserProfile.OverrideAvatar(avatarModel, face256Snapshot); |
| | 583 | |
|
| 6 | 584 | | if (dataStore.common.isSignUpFlow.Get()) |
| | 585 | | { |
| 3 | 586 | | dataStore.HUDs.signupVisible.Set(true); |
| | 587 | |
|
| 3 | 588 | | if (!isNewTermsOfServiceAndEmailSubscriptionEnabled) |
| 3 | 589 | | backpackAnalyticsService.SendAvatarEditSuccessNuxAnalytic(); |
| | 590 | | } |
| | 591 | |
|
| 6 | 592 | | avatarIsDirty = false; |
| 6 | 593 | | } |
| | 594 | |
|
| | 595 | | private void SelectWearable(string wearableId) |
| | 596 | | { |
| 0 | 597 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(wearableId, out WearableItem wearable)) |
| | 598 | | { |
| 0 | 599 | | Debug.LogError($"Cannot pre-visualize wearable {wearableId}"); |
| 0 | 600 | | return; |
| | 601 | | } |
| 0 | 602 | | } |
| | 603 | |
|
| | 604 | | private void EquipWearableFromGrid(string wearableId, EquipWearableSource source) |
| | 605 | | { |
| 13 | 606 | | AudioScriptableObjects.equip.Play(true); |
| 13 | 607 | | EquipWearable(wearableId, source); |
| 13 | 608 | | } |
| | 609 | |
|
| | 610 | | private void UnEquipWearableFromGrid(string wearableId, UnequipWearableSource source) |
| | 611 | | { |
| 3 | 612 | | AudioScriptableObjects.unequip.Play(true); |
| 3 | 613 | | UnEquipWearable(wearableId, source); |
| 3 | 614 | | } |
| | 615 | |
|
| | 616 | | private void OnCategoryFilterRemoved() => |
| 9 | 617 | | view.SetColorPickerVisibility(false); |
| | 618 | |
|
| | 619 | | private void UnEquipWearableFromSlot(string wearableId, UnequipWearableSource source) |
| | 620 | | { |
| 0 | 621 | | AudioScriptableObjects.unequip.Play(true); |
| 0 | 622 | | UnEquipWearable(wearableId, source); |
| 0 | 623 | | } |
| | 624 | |
|
| | 625 | | private void EquipWearable(string wearableId, |
| | 626 | | EquipWearableSource source, |
| | 627 | | bool setAsDirty = true, |
| | 628 | | bool updateAvatarPreview = true, |
| | 629 | | bool resetOverride = true) |
| | 630 | | { |
| 15 | 631 | | string shortenedWearableId = ExtendedUrnParser.GetShortenedUrn(wearableId); |
| | 632 | |
|
| 15 | 633 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(shortenedWearableId, out WearableItem wearable)) |
| | 634 | | { |
| 0 | 635 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(wearableId, out wearable)) |
| | 636 | | { |
| 0 | 637 | | Debug.LogError($"Cannot equip wearable {shortenedWearableId}"); |
| 0 | 638 | | return; |
| | 639 | | } |
| | 640 | | } |
| | 641 | |
|
| 15 | 642 | | EquipWearable(wearableId, wearable, source, setAsDirty, updateAvatarPreview, resetOverride); |
| 15 | 643 | | } |
| | 644 | |
|
| | 645 | | private void EquipWearable( |
| | 646 | | string extendedWearableId, |
| | 647 | | WearableItem wearable, |
| | 648 | | EquipWearableSource source, |
| | 649 | | bool setAsDirty = true, |
| | 650 | | bool updateAvatarPreview = true, |
| | 651 | | bool resetOverride = true) |
| | 652 | | { |
| 25 | 653 | | string shortenWearableId = ExtendedUrnParser.GetShortenedUrn(wearable.id); |
| 25 | 654 | | extendedWearableUrns[shortenWearableId] = extendedWearableId; |
| | 655 | |
|
| 25 | 656 | | if (wearable.data.category == WearableLiterals.Categories.BODY_SHAPE) |
| | 657 | | { |
| 5 | 658 | | UnEquipCurrentBodyShape(); |
| 5 | 659 | | EquipBodyShape(wearable); |
| 5 | 660 | | ReplaceIncompatibleWearablesWithDefaultWearables(); |
| | 661 | | } |
| | 662 | | else |
| | 663 | | { |
| 108 | 664 | | foreach (var w in model.wearables.Values) |
| | 665 | | { |
| 34 | 666 | | if (w.data.category != wearable.data.category) |
| | 667 | | continue; |
| | 668 | |
|
| 0 | 669 | | UnEquipWearable(w.id, UnequipWearableSource.None); |
| 0 | 670 | | break; |
| | 671 | | } |
| | 672 | |
|
| 20 | 673 | | model.wearables.Add(shortenWearableId, wearable); |
| 20 | 674 | | previewEquippedWearables.Add(shortenWearableId); |
| | 675 | |
|
| 20 | 676 | | if (resetOverride) |
| 10 | 677 | | ResetOverridesOfAffectedCategories(wearable, setAsDirty); |
| | 678 | |
|
| 20 | 679 | | avatarSlotsHUDController.Equip(wearable, model.bodyShape.id, model.forceRender); |
| 20 | 680 | | wearableGridController.Equip(shortenWearableId); |
| | 681 | | } |
| | 682 | |
|
| 25 | 683 | | if (setAsDirty) |
| 15 | 684 | | avatarIsDirty = true; |
| | 685 | |
|
| 25 | 686 | | if (source != EquipWearableSource.None) |
| 13 | 687 | | backpackAnalyticsService.SendEquipWearableAnalytic(wearable.id, wearable.data.category, wearable.rarity, |
| | 688 | |
|
| 25 | 689 | | if (updateAvatarPreview) |
| | 690 | | { |
| 13 | 691 | | UpdateAvatarModel(model.ToAvatarModel()); |
| 13 | 692 | | categoryPendingToPlayEmote = wearable.data.category; |
| | 693 | | } |
| | 694 | |
|
| 25 | 695 | | if (wearable.data.blockVrmExport && !vrmBlockingWearables.ContainsKey(shortenWearableId)) |
| | 696 | | { |
| 0 | 697 | | vrmBlockingWearables.Add(shortenWearableId, wearable.CanBeUnEquipped()); |
| 0 | 698 | | UpdateVRMExportWarning(); |
| | 699 | | } |
| 25 | 700 | | } |
| | 701 | |
|
| | 702 | | private void ReplaceIncompatibleWearablesWithDefaultWearables() |
| | 703 | | { |
| 5 | 704 | | WearableItem bodyShape = model.bodyShape; |
| | 705 | |
|
| 5 | 706 | | if (bodyShape == null) return; |
| 5 | 707 | | if (!fallbackWearables.ContainsKey(bodyShape.id)) return; |
| | 708 | |
|
| 5 | 709 | | HashSet<string> replacedCategories = new (); |
| | 710 | |
|
| 16 | 711 | | foreach (var w in model.wearables.Values.ToArray()) |
| | 712 | | { |
| 3 | 713 | | if (w.SupportsBodyShape(bodyShape.id)) continue; |
| | 714 | |
|
| 2 | 715 | | UnEquipWearable(w, UnequipWearableSource.None, true, false); |
| | 716 | |
|
| 2 | 717 | | string category = w.data.category; |
| | 718 | |
|
| 2 | 719 | | if (!string.IsNullOrEmpty(category)) |
| 2 | 720 | | replacedCategories.Add(category); |
| | 721 | | } |
| | 722 | |
|
| 5 | 723 | | Dictionary<string, string> fallbackWearablesByCategory = fallbackWearables[bodyShape.id]; |
| | 724 | |
|
| 14 | 725 | | foreach (string category in replacedCategories) |
| | 726 | | { |
| 2 | 727 | | if (!fallbackWearablesByCategory.ContainsKey(category)) continue; |
| 2 | 728 | | EquipWearable(fallbackWearablesByCategory[category], EquipWearableSource.None, true, false); |
| | 729 | | } |
| 5 | 730 | | } |
| | 731 | |
|
| | 732 | | private void UnEquipCurrentBodyShape(bool setAsDirty = true) |
| | 733 | | { |
| 12 | 734 | | if (model.bodyShape.id == NOT_LOADED) return; |
| | 735 | |
|
| 2 | 736 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(model.bodyShape.id, out WearableItem wearable)) |
| | 737 | | { |
| 0 | 738 | | Debug.LogError($"Cannot unequip body shape {model.bodyShape.id}"); |
| 0 | 739 | | return; |
| | 740 | | } |
| | 741 | |
|
| 2 | 742 | | UnEquipWearable(wearable, UnequipWearableSource.None, setAsDirty); |
| 2 | 743 | | } |
| | 744 | |
|
| | 745 | | private void UnEquipWearable(string wearableId, UnequipWearableSource source, bool setAsDirty = true, bool updat |
| | 746 | | { |
| 3 | 747 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(wearableId, out WearableItem wearable)) |
| | 748 | | { |
| 0 | 749 | | Debug.LogError($"Cannot unequip wearable {wearableId}"); |
| 0 | 750 | | return; |
| | 751 | | } |
| | 752 | |
|
| 3 | 753 | | UnEquipWearable(wearable, source, setAsDirty, updateAvatarPreview); |
| 3 | 754 | | } |
| | 755 | |
|
| | 756 | | private void UnEquipWearable(WearableItem wearable, |
| | 757 | | UnequipWearableSource source = UnequipWearableSource.None, |
| | 758 | | bool setAsDirty = true, |
| | 759 | | bool updateAvatarPreview = true) |
| | 760 | | { |
| 7 | 761 | | string shortenedWearableId = ExtendedUrnParser.GetShortenedUrn(wearable.id); |
| | 762 | |
|
| 7 | 763 | | if (source != UnequipWearableSource.None) |
| 0 | 764 | | backpackAnalyticsService.SendUnequippedWearableAnalytic(wearable.id, wearable.data.category, wearable.ra |
| | 765 | |
|
| 7 | 766 | | ResetOverridesOfAffectedCategories(wearable, setAsDirty); |
| | 767 | |
|
| 7 | 768 | | avatarSlotsHUDController.UnEquip(wearable.data.category, model.forceRender); |
| 7 | 769 | | model.wearables.Remove(shortenedWearableId); |
| 7 | 770 | | previewEquippedWearables.Remove(shortenedWearableId); |
| 7 | 771 | | wearableGridController.UnEquip(shortenedWearableId); |
| | 772 | |
|
| 7 | 773 | | if (setAsDirty) |
| 7 | 774 | | avatarIsDirty = true; |
| | 775 | |
|
| 7 | 776 | | if (updateAvatarPreview) |
| 5 | 777 | | UpdateAvatarModel(model.ToAvatarModel()); |
| | 778 | |
|
| 7 | 779 | | if (wearable.data.blockVrmExport) |
| | 780 | | { |
| 0 | 781 | | if (vrmBlockingWearables.Remove(shortenedWearableId)) |
| 0 | 782 | | UpdateVRMExportWarning(); |
| | 783 | | } |
| 7 | 784 | | } |
| | 785 | |
|
| | 786 | | private void UpdateVRMExportWarning() |
| | 787 | | { |
| 25 | 788 | | bool vrmWarningEnabled = vrmBlockingWearables.Count > 0; |
| 25 | 789 | | view.SetWarningForVRMExportButton(vrmWarningEnabled); |
| 25 | 790 | | } |
| | 791 | |
|
| | 792 | | private void ResetOverridesOfAffectedCategories(WearableItem wearable, bool setAsDirty = true) |
| | 793 | | { |
| 17 | 794 | | if (wearable.GetHidesList(ownUserProfile.avatar.bodyShape) != null) |
| 44 | 795 | | foreach (string s in wearable.GetHidesList(ownUserProfile.avatar.bodyShape)) |
| 5 | 796 | | UpdateOverrideHides(s, false, setAsDirty); |
| 17 | 797 | | } |
| | 798 | |
|
| | 799 | | private void ToggleSlot(string slotCategory, bool supportColor, PreviewCameraFocus previewCameraFocus, bool isSe |
| | 800 | | { |
| 3 | 801 | | currentSlotSelected = isSelected ? slotCategory : null; |
| 3 | 802 | | view.UpdateHideUnhideStatus(currentSlotSelected, model.forceRender); |
| 3 | 803 | | view.SetColorPickerVisibility(isSelected && supportColor); |
| | 804 | |
|
| 3 | 805 | | if (isSelected && supportColor) |
| 3 | 806 | | view.SetColorPickerAsSkinMode(slotCategory == WearableLiterals.Categories.BODY_SHAPE); |
| | 807 | |
|
| | 808 | | switch (slotCategory) |
| | 809 | | { |
| | 810 | | case WearableLiterals.Categories.EYES: |
| 1 | 811 | | view.SetColorPickerValue(model.eyesColor); |
| 1 | 812 | | break; |
| | 813 | | case WearableLiterals.Categories.HAIR or WearableLiterals.Categories.EYEBROWS or WearableLiterals.Catego |
| 0 | 814 | | view.SetColorPickerValue(model.hairColor); |
| 0 | 815 | | break; |
| | 816 | | case WearableLiterals.Categories.BODY_SHAPE: |
| 1 | 817 | | view.SetColorPickerValue(model.skinColor); |
| | 818 | | break; |
| | 819 | | } |
| | 820 | |
|
| 3 | 821 | | view.SetAvatarPreviewFocus(currentSlotSelected != null ? previewCameraFocus : PreviewCameraFocus.DefaultEdit |
| 3 | 822 | | } |
| | 823 | |
|
| | 824 | | private void OnWearableColorChanged(Color newColor) |
| | 825 | | { |
| 0 | 826 | | var colorChanged = false; |
| | 827 | |
|
| 0 | 828 | | switch (currentSlotSelected) |
| | 829 | | { |
| | 830 | | case WearableLiterals.Categories.EYES: |
| 0 | 831 | | model.eyesColor = newColor; |
| 0 | 832 | | colorChanged = true; |
| 0 | 833 | | break; |
| | 834 | | case WearableLiterals.Categories.HAIR or WearableLiterals.Categories.EYEBROWS or WearableLiterals.Catego |
| 0 | 835 | | model.hairColor = newColor; |
| 0 | 836 | | colorChanged = true; |
| 0 | 837 | | break; |
| | 838 | | case WearableLiterals.Categories.BODY_SHAPE: |
| 0 | 839 | | model.skinColor = newColor; |
| 0 | 840 | | colorChanged = true; |
| | 841 | | break; |
| | 842 | | } |
| | 843 | |
|
| 0 | 844 | | if (!colorChanged) |
| 0 | 845 | | return; |
| | 846 | |
|
| 0 | 847 | | avatarIsDirty = true; |
| 0 | 848 | | UpdateAvatarModel(model.ToAvatarModel()); |
| 0 | 849 | | } |
| | 850 | |
|
| | 851 | | private void SetAllColors(Color eyesColor, Color hairColor, Color bodyColor) |
| | 852 | | { |
| 0 | 853 | | model.eyesColor = eyesColor; |
| 0 | 854 | | model.hairColor = hairColor; |
| 0 | 855 | | model.skinColor = bodyColor; |
| | 856 | |
|
| 0 | 857 | | avatarIsDirty = true; |
| 0 | 858 | | UpdateAvatarModel(model.ToAvatarModel()); |
| 0 | 859 | | } |
| | 860 | |
|
| | 861 | | private void UpdateAvatarModel(AvatarModel avatarModel) |
| | 862 | | { |
| 26 | 863 | | view.UpdateAvatarPreview(avatarModel); |
| 26 | 864 | | outfitsController.UpdateAvatarPreview(model.ToAvatarModel(extendedWearableUrns)); |
| 26 | 865 | | } |
| | 866 | |
|
| | 867 | | private void OnColorPickerToggled() => |
| 0 | 868 | | backpackAnalyticsService.SendAvatarColorPick(); |
| | 869 | |
|
| | 870 | | private void OnAvatarUpdated() |
| | 871 | | { |
| 0 | 872 | | if (string.IsNullOrEmpty(categoryPendingToPlayEmote)) |
| 0 | 873 | | return; |
| | 874 | |
|
| 0 | 875 | | PlayEquipAnimation(categoryPendingToPlayEmote); |
| 0 | 876 | | categoryPendingToPlayEmote = null; |
| 0 | 877 | | } |
| | 878 | |
|
| | 879 | | private void PlayEquipAnimation(string category) |
| | 880 | | { |
| 0 | 881 | | view.PlayPreviewEmote( |
| | 882 | | GetEquipEmoteByCategory(category), |
| | 883 | | DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); |
| 0 | 884 | | } |
| | 885 | |
|
| | 886 | | private string GetEquipEmoteByCategory(string category) |
| | 887 | | { |
| 0 | 888 | | string equipEmote = category switch |
| | 889 | | { |
| 0 | 890 | | WearableLiterals.Categories.FEET => GetRandomizedName(WearableLiterals.DefaultEmotes |
| 0 | 891 | | WearableLiterals.Categories.LOWER_BODY => GetRandomizedName(WearableLiterals.Default |
| 0 | 892 | | WearableLiterals.Categories.UPPER_BODY => GetRandomizedName(WearableLiterals.Default |
| 0 | 893 | | WearableLiterals.Categories.EYEWEAR => GetRandomizedName(WearableLiterals.DefaultEmo |
| 0 | 894 | | WearableLiterals.Categories.TIARA => GetRandomizedName(WearableLiterals.DefaultEmote |
| 0 | 895 | | WearableLiterals.Categories.EARRING => GetRandomizedName(WearableLiterals.DefaultEmo |
| 0 | 896 | | WearableLiterals.Categories.HAT => GetRandomizedName(WearableLiterals.DefaultEmotes. |
| 0 | 897 | | WearableLiterals.Categories.TOP_HEAD => GetRandomizedName(WearableLiterals.DefaultEm |
| 0 | 898 | | WearableLiterals.Categories.HELMET => GetRandomizedName(WearableLiterals.DefaultEmot |
| 0 | 899 | | WearableLiterals.Categories.MASK => GetRandomizedName(WearableLiterals.DefaultEmotes |
| 0 | 900 | | WearableLiterals.Categories.SKIN => GetRandomizedName(WearableLiterals.DefaultEmotes |
| 0 | 901 | | _ => string.Empty, |
| | 902 | | }; |
| | 903 | |
|
| 0 | 904 | | return equipEmote; |
| | 905 | | } |
| | 906 | |
|
| | 907 | | private string GetRandomizedName(string baseString, int limit) |
| | 908 | | { |
| 0 | 909 | | currentAnimationIndexShown = (currentAnimationIndexShown + 1) % limit; |
| 0 | 910 | | return $"{baseString}{(currentAnimationIndexShown + 1).ToString()}"; |
| | 911 | | } |
| | 912 | |
|
| | 913 | | private void OnVrmExport() |
| | 914 | | { |
| 0 | 915 | | if(vrmBlockingWearables.Count > 0) return; |
| | 916 | |
|
| 0 | 917 | | vrmExportCts?.SafeCancelAndDispose(); |
| 0 | 918 | | vrmExportCts = new CancellationTokenSource(); |
| 0 | 919 | | VrmExport(vrmExportCts.Token).Forget(); |
| 0 | 920 | | } |
| | 921 | |
|
| | 922 | | private void OnSignUpBack(SignUpStage stage) |
| | 923 | | { |
| | 924 | | switch (stage) |
| | 925 | | { |
| | 926 | | default: |
| | 927 | | case SignUpStage.CustomizeAvatar: |
| 1 | 928 | | userProfileBridge.LogOut(); |
| 1 | 929 | | break; |
| | 930 | | case SignUpStage.SetNameAndEmail: |
| 1 | 931 | | dataStore.HUDs.signupVisible.Set(false); |
| 1 | 932 | | view.SetSignUpStage(SignUpStage.CustomizeAvatar); |
| 1 | 933 | | view.SetAvatarPreviewFocus(PreviewCameraFocus.DefaultEditing); |
| | 934 | | break; |
| | 935 | | } |
| 1 | 936 | | } |
| | 937 | |
|
| | 938 | | internal async UniTask VrmExport(CancellationToken ct) |
| | 939 | | { |
| | 940 | | const int SUCCESS_TOAST_ACTIVE_TIME = 2000; |
| | 941 | |
|
| | 942 | | try |
| | 943 | | { |
| 1 | 944 | | view?.SetVRMButtonEnabled(false); |
| 1 | 945 | | view?.SetVRMSuccessToastActive(false); |
| | 946 | |
|
| 1 | 947 | | backpackAnalyticsService.SendVRMExportStarted(); |
| | 948 | |
|
| 1 | 949 | | StringBuilder reference = new StringBuilder(); |
| | 950 | |
|
| | 951 | | try |
| | 952 | | { |
| 1 | 953 | | var wearables = await this.ownUserProfile.avatar.wearables.Select(x => this.wearablesCatalogService. |
| | 954 | |
|
| 2 | 955 | | foreach (WearableItem wearableItem in wearables) |
| | 956 | | { |
| 0 | 957 | | reference.AppendLine(string.Join(":", |
| | 958 | | wearableItem.data.category, |
| | 959 | | wearableItem.GetName(), |
| | 960 | | wearableItem.GetMarketplaceLink() |
| | 961 | | )); |
| | 962 | | } |
| 1 | 963 | | } |
| 0 | 964 | | catch (Exception) |
| | 965 | | { |
| | 966 | | // ignored |
| 0 | 967 | | } |
| | 968 | |
|
| 1 | 969 | | byte[] bytes = await vrmExporter.Export($"{this.ownUserProfile.userName} Avatar", reference.ToString(), |
| | 970 | |
|
| 1 | 971 | | string fileName = $"{this.ownUserProfile.userName.Replace("#", "_")}_{DateTime.Now.ToString("yyyyMMddhhm |
| 1 | 972 | | await fileBrowser.SaveFileAsync("Save your VRM", Application.persistentDataPath, fileName, bytes, new Ex |
| | 973 | |
|
| 1 | 974 | | view?.SetVRMSuccessToastActive(true); |
| 3 | 975 | | await UniTask.Delay(SUCCESS_TOAST_ACTIVE_TIME, cancellationToken: ct); |
| 1 | 976 | | } |
| 0 | 977 | | catch (OperationCanceledException) { } |
| | 978 | | finally |
| | 979 | | { |
| 1 | 980 | | view?.SetVRMButtonEnabled(true); |
| 1 | 981 | | view?.SetVRMSuccessToastActive(false); |
| | 982 | | } |
| | 983 | |
|
| 1 | 984 | | backpackAnalyticsService.SendVRMExportSucceeded(); |
| 1 | 985 | | } |
| | 986 | | } |
| | 987 | | } |