Пример #1
0
        // called from game thread
        public bool HasInvitationFromNotification()
        {
            bool has = mInvitationFromNotification != null;

            Logger.d("AndroidClient.HasInvitationFromNotification, returning " + has);
            return(has);
        }
Пример #2
0
        // called on game thread
        public void CreateWithInvitationScreen(int minOpponents, int maxOpponents,
                                               int variant, Action <bool, TurnBasedMatch> callback)
        {
            Logger.d(string.Format("AndroidTbmpClient.CreateWithInvitationScreen, " +
                                   "opponents {0}-{1}, variant {2}", minOpponents, maxOpponents,
                                   variant));

            mClient.CallClientApi("tbmp launch invitation screen", () => {
                AndroidJavaClass klass = JavaUtil.GetClass(
                    JavaConsts.SupportSelectOpponentsHelperActivity);
                klass.CallStatic("launch", false, mClient.GetActivity(),
                                 new SelectOpponentsProxy(this, callback, variant),
                                 Logger.DebugLogEnabled,
                                 minOpponents, maxOpponents);
            }, (bool success) => {
                if (!success)
                {
                    Logger.w("Failed to create tbmp w/ invite screen: client disconnected.");
                    if (callback != null)
                    {
                        callback.Invoke(false, null);
                    }
                }
            });
        }
Пример #3
0
        // called on game thread
        public void CreateQuickMatch(int minOpponents, int maxOpponents, int variant,
                                     Action <bool, TurnBasedMatch> callback)
        {
            Logger.d(string.Format("AndroidTbmpClient.CreateQuickMatch, opponents {0}-{1}, var {2}",
                                   minOpponents, maxOpponents, variant));

            mClient.CallClientApi("tbmp create quick game", () => {
                ResultProxy proxy = new ResultProxy(this, "createMatch");
                proxy.SetMatchCallback(callback);
                AndroidJavaClass tbmpUtil = JavaUtil.GetClass(JavaConsts.SupportTbmpUtilsClass);
                using (AndroidJavaObject pendingResult = tbmpUtil.CallStatic <AndroidJavaObject>(
                           "createQuickMatch", mClient.GHManager.GetApiClient(),
                           minOpponents, maxOpponents, variant)) {
                    pendingResult.Call("setResultCallback", proxy);
                }
            }, (bool success) => {
                if (!success)
                {
                    Logger.w("Failed to create tbmp quick match: client disconnected.");
                    if (callback != null)
                    {
                        callback.Invoke(false, null);
                    }
                }
            });
        }
Пример #4
0
        private void OnAuthenticated()
        {
            GooglePlayGames.OurUtils.PlayGamesHelperObject.RunOnGameThread(
                () =>
            {
                cloudOnceEvents.RaiseOnSignedInChanged(true);
                Logger.d("Successfully signed in to Google Play Game Services. Player: " + PlayerDisplayName);
                IsGuestUserDefault = false;
                GetPlayerImage();
                if (playerIdCache != null && !string.Equals(playerIdCache, PlayerID, StringComparison.InvariantCulture))
                {
                    // Switching user
                    foreach (var achievement in Achievements.All)
                    {
                        achievement.ResetLocalState();
                    }

                    if (cloudSaveEnabled)
                    {
                        GooglePlayGamesCloudSaveWrapper.LoadDataString(OnDataStringLoaded);
                    }
                }
                else if (autoLoadEnabled && cloudSaveEnabled)
                {
                    Cloud.Storage.Load();
                }

                playerIdCache = PlayerID;
                if (Achievements.All.Length > 0)
                {
                    PlayGamesPlatform.Instance.LoadAchievements(UpdateAchievementsData);
                }
            });
        }
Пример #5
0
        // called from game thread
        public void UnlockAchievement(string achId, Action <bool> callback)
        {
            // if the local cache says it's unlocked, we don't have to do anything
            Logger.d("AndroidClient.UnlockAchievement: " + achId);
            Achievement a = GetAchievement(achId);

            if (a != null && a.IsUnlocked)
            {
                Logger.d("...was already unlocked, so no-op.");
                if (callback != null)
                {
                    callback.Invoke(true);
                }
                return;
            }

            CallClientApi("unlock ach " + achId, () => {
                mGHManager.CallGmsApi("games.Games", "Achievements", "unlock", achId);
            }, callback);

            // update local cache
            a = GetAchievement(achId);
            if (a != null)
            {
                a.IsUnlocked = a.IsRevealed = true;
            }
        }
Пример #6
0
 internal void CallClientApi(string desc, Action call, Action <bool> callback)
 {
     Logger.d("Requesting API call: " + desc);
     RunWhenConnectionStable(() => {
         // we got a stable connection state to the games service
         // (either connected or disconnected, but not in progress).
         if (mGHManager.IsConnected())
         {
             // we are connected, so make the API call
             Logger.d("Connected! Calling API: " + desc);
             call.Invoke();
             if (callback != null)
             {
                 PlayGamesHelperObject.RunOnGameThread(() => {
                     callback.Invoke(true);
                 });
             }
         }
         else
         {
             // we are not connected, so fail the API call
             Logger.w("Not connected! Failed to call API :" + desc);
             if (callback != null)
             {
                 PlayGamesHelperObject.RunOnGameThread(() => {
                     callback.Invoke(false);
                 });
             }
         }
     });
 }
Пример #7
0
        // called from game thread
        public void RevealAchievement(string achId, Action <bool> callback)
        {
            Logger.d("AndroidClient.RevealAchievement: " + achId);
            Achievement a = GetAchievement(achId);

            if (a != null && a.IsRevealed)
            {
                Logger.d("...was already revealed, so no-op.");
                if (callback != null)
                {
                    callback.Invoke(true);
                }
                return;
            }

            CallClientApi("reveal ach " + achId, () => {
                mGHManager.CallGmsApi("games.Games", "Achievements", "reveal", achId);
            }, callback);

            // update local cache
            a = GetAchievement(achId);
            if (a != null)
            {
                a.IsRevealed = true;
            }
        }
Пример #8
0
        // called (on the UI thread) by GameHelperManager to notify us that sign in succeeded
        internal void OnSignInSucceeded()
        {
            Logger.d("AndroidClient got OnSignInSucceeded.");
            RetrieveUserInfo();

            if (mAuthState == AuthState.AuthPending || mAuthState == AuthState.InProgress)
            {
                Logger.d("AUTH: Auth succeeded. Proceeding to achievement loading.");
                DoInitialAchievementLoad();
            }
            else if (mAuthState == AuthState.LoadingAchs)
            {
                Logger.w("AUTH: Got OnSignInSucceeded() while in achievement loading phase (unexpected).");
                Logger.w("AUTH: Trying to fix by issuing a new achievement load call.");
                DoInitialAchievementLoad();
            }
            else
            {
                // we will hit this case during the normal lifecycle (for example, Activity
                // was brought to the foreground and sign in has succeeded even though
                // we were not in an auth flow).
                Logger.d("Normal lifecycle OnSignInSucceeded received.");
                RunPendingActions();

                // check for invitations that may have arrived via notification
                CheckForConnectionExtras();

                // inform the RTMP client that sign-in has suceeded
                mRtmpClient.OnSignInSucceeded();
                mTbmpClient.OnSignInSucceeded();
            }
        }
Пример #9
0
        // called from game thread
        public void RegisterInvitationDelegate(InvitationReceivedDelegate deleg)
        {
            Logger.d("AndroidClient.RegisterInvitationDelegate");
            if (deleg == null)
            {
                Logger.w("AndroidClient.RegisterInvitationDelegate called w/ null argument.");
                return;
            }
            mInvitationDelegate = deleg;

            // install invitation listener, if we don't have one yet
            if (!mRegisteredInvitationListener)
            {
                Logger.d("Registering an invitation listener.");
                RegisterInvitationListener();
            }

            if (mInvitationFromNotification != null)
            {
                Logger.d("Delivering pending invitation from notification now.");
                Invitation inv = mInvitationFromNotification;
                mInvitationFromNotification = null;
                PlayGamesHelperObject.RunOnGameThread(() => {
                    if (mInvitationDelegate != null)
                    {
                        mInvitationDelegate.Invoke(inv, true);
                    }
                });
            }
        }
Пример #10
0
        /// <summary>
        /// Used internally when switching user.
        /// </summary>
        /// <param name="onDataLoaded">Action to invoke when data has been loaded.</param>
        public static void LoadDataString(UnityAction <string> onDataLoaded)
        {
#if CLOUDONCE_DEBUG
            Logger.d("Switching user. Loading default save game.");
#endif
            PlayGamesPlatform.Instance.SavedGame.OpenWithAutomaticConflictResolution(
                saveGameFileName,
                DataSource.ReadCacheOrNetwork,
                ConflictResolutionStrategy.UseLongestPlaytime,
                (status, metadata) =>
            {
                if (status == SavedGameRequestStatus.Success)
                {
                    PlayGamesPlatform.Instance.SavedGame.ReadBinaryData(
                        metadata,
                        (requestStatus, bytes) =>
                    {
                        if (requestStatus == SavedGameRequestStatus.Success)
                        {
                            s_timeWhenCloudSaveWasLoaded = Time.realtimeSinceStartup;
                            var dataString = GetDataString(bytes);
                            onDataLoaded.Invoke(dataString);
                        }
                        else
                        {
                            onDataLoaded.Invoke(null);
                        }
                    });
                }
                else
                {
                    onDataLoaded.Invoke(null);
                }
            });
        }
Пример #11
0
        public void Finish(string matchId, byte[] data, MatchOutcome outcome, Action <bool> callback)
        {
            Logger.d(string.Format("AndroidTbmpClient.Finish matchId={0}, data={1} outcome={2}",
                                   matchId, data == null ? "(null)" : data.Length + " bytes", outcome));

            Logger.d("Preparing list of participant results as Android ArrayList.");
            AndroidJavaObject participantResults = new AndroidJavaObject("java.util.ArrayList");

            if (outcome != null)
            {
                foreach (string pid in outcome.ParticipantIds)
                {
                    Logger.d("Converting participant result to Android object: " + pid);
                    AndroidJavaObject thisParticipantResult = new AndroidJavaObject(
                        JavaConsts.ParticipantResultClass, pid,
                        JavaUtil.GetAndroidParticipantResult(outcome.GetResultFor(pid)),
                        outcome.GetPlacementFor(pid));

                    // (yes, the return type of ArrayList.add is bool, strangely)
                    Logger.d("Adding participant result to Android ArrayList.");
                    participantResults.Call <bool>("add", thisParticipantResult);
                    thisParticipantResult.Dispose();
                }
            }

            TbmpApiCall("tbmp finish w/ outcome", "finishMatch", callback, null,
                        matchId, data, participantResults);
        }
Пример #12
0
        /// <summary>
        /// Initializes Google Play Game Services.
        /// </summary>
        /// <param name="activateCloudSave">Whether or not Cloud Saving should be activated.</param>
        /// <param name="autoSignIn">
        /// Whether or not <see cref="SignIn"/> will be called automatically once Google Play Game Services is initialized.
        /// </param>
        /// <param name="autoCloudLoad">
        /// Whether or not cloud data should be loaded automatically if the user is successfully signed in.
        /// Ignored if Cloud Saving is deactivated or the user fails to sign in.
        /// </param>
        public override void Initialize(bool activateCloudSave = true, bool autoSignIn = true, bool autoCloudLoad = true)
        {
            if (initializing)
            {
                return;
            }
#if CLOUDONCE_DEBUG
            Debug.Log("Initializing Google Play Game Services.");
#endif
            initializing = true;

            cloudSaveEnabled = activateCloudSave;

#if CLOUDONCE_DEBUG
            Debug.Log("Saved Games support " + (activateCloudSave ? "enabled." : "disabled."));
#endif
            var config = new PlayGamesClientConfiguration.Builder();
            if (activateCloudSave)
            {
                config.EnableSavedGames();
                CloudSaveInitialized = true;
            }

            PlayGamesPlatform.InitializeInstance(config.Build());

            SubscribeOnAuthenticatedEvent();

#if CLOUDONCE_DEBUG   // Enable/disable logs on the PlayGamesPlatform
            PlayGamesPlatform.DebugLogEnabled = true;
            Debug.Log("PlayGamesPlatform debug logs enabled.");
#else
            PlayGamesPlatform.DebugLogEnabled = false;
            Debug.Log("PlayGamesPlatform debug logs disabled.");
#endif
            IsGpgsInitialized = true;
            if (!IsGuestUserDefault && autoSignIn)
            {
                var onSignedIn = new UnityAction <bool>(arg0 =>
                {
                    cloudOnceEvents.RaiseOnInitializeComplete();
                    initializing = false;
                });
                SignIn(autoCloudLoad, onSignedIn);
            }
            else
            {
                if (IsGuestUserDefault && autoSignIn)
                {
                    Logger.d("Guest user mode active, ignoring auto sign-in. Please call SignIn directly.");
                }

                if (autoCloudLoad)
                {
                    cloudOnceEvents.RaiseOnCloudLoadComplete(false);
                }

                cloudOnceEvents.RaiseOnInitializeComplete();
                initializing = false;
            }
        }
Пример #13
0
 /// <summary>
 /// Activates the Play Games platform as the implementation of Social.Active.
 /// After calling this method, you can call methods on Social.Active. For
 /// example, <c>Social.Active.Authenticate()</c>.
 /// </summary>
 /// <returns>The singleton <see cref="PlayGamesPlatform" /> instance.</returns>
 public static PlayGamesPlatform Activate()
 {
     Logger.d("Activating PlayGamesPlatform.");
     Social.Active = PlayGamesPlatform.Instance;
     Logger.d("PlayGamesPlatform activated: " + Social.Active);
     return(PlayGamesPlatform.Instance);
 }
Пример #14
0
        private Invitation ConvertInvitation(AndroidJavaObject invObj)
        {
            Logger.d("Converting Android invitation to our Invitation object.");
            string      invitationId = invObj.Call <string>("getInvitationId");
            int         invType      = invObj.Call <int>("getInvitationType");
            Participant inviter;

            using (AndroidJavaObject inviterObj = invObj.Call <AndroidJavaObject>("getInviter")) {
                inviter = JavaUtil.ConvertParticipant(inviterObj);
            }
            int variant = invObj.Call <int>("getVariant");

            Invitation.InvType type;

            switch (invType)
            {
            case JavaConsts.INVITATION_TYPE_REAL_TIME:
                type = Invitation.InvType.RealTime;
                break;

            case JavaConsts.INVITATION_TYPE_TURN_BASED:
                type = Invitation.InvType.TurnBased;
                break;

            default:
                Logger.e("Unknown invitation type " + invType);
                type = Invitation.InvType.Unknown;
                break;
            }

            Invitation result = new Invitation(type, invitationId, inviter, variant);

            Logger.d("Converted invitation: " + result.ToString());
            return(result);
        }
Пример #15
0
 // called from game thread
 public Invitation GetInvitationFromNotification()
 {
     Logger.d("AndroidClient.GetInvitationFromNotification");
     Logger.d("Returning invitation: " + ((mInvitationFromNotification == null) ?
                                          "(null)" : mInvitationFromNotification.ToString()));
     return(mInvitationFromNotification);
 }
Пример #16
0
        public static void ShowSelectSnapshotUI(bool showCreateSaveUI, bool showDeleteSaveUI,
                                                int maxDisplayedSavedGames, string uiTitle, Action <SelectUIStatus, ISavedGameMetadata> cb)
        {
            using (var helperFragment = new AndroidJavaClass(HelperFragmentClass))
                using (var task = helperFragment.CallStatic <AndroidJavaObject>("showSelectSnapshotUi",
                                                                                GetActivity(), uiTitle, showCreateSaveUI, showDeleteSaveUI,
                                                                                maxDisplayedSavedGames))
                {
                    AndroidTaskUtils.AddOnSuccessListener <AndroidJavaObject>(
                        task,
                        result =>
                    {
                        var status = (SelectUIStatus)result.Get <int>("status");
                        Logger.d("ShowSelectSnapshotUI result " + status);

                        var javaMetadata = result.Get <AndroidJavaObject>("metadata");
                        var metadata     =
                            javaMetadata == null
                                ? null
                                : new AndroidSnapshotMetadata(javaMetadata, /* contents= */ null);

                        cb.Invoke(status, metadata);
                    });

                    AndroidTaskUtils.AddOnFailureListener(
                        task,
                        exception =>
                    {
                        Logger.e("ShowSelectSnapshotUI failed with exception");
                        cb.Invoke(SelectUIStatus.InternalError, null);
                    });
                }
        }
Пример #17
0
 public void LeaveDuringTurn(string matchId, string pendingParticipantId,
                             Action <bool> callback)
 {
     Logger.d("AndroidTbmpClient.LeaveDuringTurn, matchId=" + matchId + ", pending=" +
              pendingParticipantId);
     TbmpApiCall("tbmp leave during turn", "leaveMatchDuringTurn", callback, null, matchId,
                 pendingParticipantId);
 }
Пример #18
0
 // called on UI thread
 public void OnSignInSucceeded()
 {
     Logger.d("AndroidTbmpClient.OnSignInSucceeded");
     Logger.d("Querying for max match data size...");
     mMaxMatchDataSize = mClient.GHManager.CallGmsApi <int>("games.Games",
                                                            "TurnBasedMultiplayer", "getMaxMatchDataSize");
     Logger.d("Max match data size: " + mMaxMatchDataSize);
 }
Пример #19
0
 // call from UI thread only!
 private void DoInitialAchievementLoad()
 {
     Logger.d("AUTH: Now performing initial achievement load...");
     mAuthState = AuthState.LoadingAchs;
     mGHManager.CallGmsApiWithResult("games.Games", "Achievements", "load",
                                     new OnAchievementsLoadedResultProxy(this), false);
     Logger.d("AUTH: Initial achievement load call made.");
 }
Пример #20
0
 // called from game thread
 public void SubmitScore(string lbId, long score, Action <bool> callback)
 {
     Logger.d("AndroidClient.SubmitScore, lb=" + lbId + ", score=" + score);
     CallClientApi("submit score " + score + ", lb " + lbId, () => {
         mGHManager.CallGmsApi("games.Games", "Leaderboards",
                               "submitScore", lbId, score);
     }, callback);
 }
Пример #21
0
 /// <summary>
 /// Sets the default leaderboard for the leaderboard UI. After calling this
 /// method, a call to <see cref="ShowLeaderboardUI" /> will show only the specified
 /// leaderboard instead of showing the list of all leaderboards.
 /// </summary>
 /// <param name='lbid'>
 /// The ID of the leaderboard to display on the default UI. This may be a raw
 /// Google Play Games leaderboard ID or an alias configured through a call to
 /// <see cref="AddIdMapping" />.
 /// </param>
 public void SetDefaultLeaderboardForUI(string lbid)
 {
     Logger.d("SetDefaultLeaderboardForUI: " + lbid);
     if (lbid != null)
     {
         lbid = MapId(lbid);
     }
     mDefaultLbUi = lbid;
 }
Пример #22
0
 // called from game thread
 public void LoadState(int slot, OnStateLoadedListener listener)
 {
     Logger.d("AndroidClient.LoadState, slot=" + slot);
     CallClientApi("load state slot=" + slot, () => {
         OnStateResultProxy proxy = new OnStateResultProxy(this, listener);
         mGHManager.CallGmsApiWithResult("appstate.AppStateManager", null, "load",
                                         proxy, slot);
     }, null);
 }
Пример #23
0
 private void RegisterInvitationListener()
 {
     Logger.d("AndroidClient.RegisterInvitationListener");
     CallClientApi("register invitation listener", () => {
         mGHManager.CallGmsApi("games.Games", "Invitations",
                               "registerInvitationListener", new OnInvitationReceivedProxy(this));
     }, null);
     mRegisteredInvitationListener = true;
 }
Пример #24
0
 // called from game thread. This is ONLY called internally (OnStateLoadedProxy
 // calls this). This is not part of the IPlayGamesClient interface.
 internal void ResolveState(int slot, string resolvedVersion, byte[] resolvedData,
                            OnStateLoadedListener listener)
 {
     Logger.d(string.Format("AndroidClient.ResolveState, slot={0}, ver={1}, " +
                            "data={2}", slot, resolvedVersion, resolvedData));
     CallClientApi("resolve state slot=" + slot, () => {
         mGHManager.CallGmsApiWithResult("appstate.AppStateManager", null, "resolve",
                                         new OnStateResultProxy(this, listener), slot, resolvedVersion, resolvedData);
     }, null);
 }
Пример #25
0
 /// <summary>
 /// Shows the standard Google Play Games leaderboards user interface,
 /// which allows the player to browse their leaderboards. If you have
 /// configured a specific leaderboard as the default through a call to
 /// <see cref="SetDefaultLeaderboardForUi" />, the UI will show that
 /// specific leaderboard only. Otherwise, a list of all the leaderboards
 /// will be shown.
 /// </summary>
 public void ShowLeaderboardUI()
 {
     if (!IsAuthenticated())
     {
         Logger.e("ShowLeaderboardUI can only be called after authentication.");
         return;
     }
     Logger.d("ShowLeaderboardUI");
     mClient.ShowLeaderboardUI(MapId(mDefaultLbUi));
 }
Пример #26
0
 internal void ClearInvitationIfFromNotification(string invitationId)
 {
     Logger.d("AndroidClient.ClearInvitationIfFromNotification: " + invitationId);
     if (mInvitationFromNotification != null &&
         mInvitationFromNotification.InvitationId.Equals(invitationId))
     {
         Logger.d("Clearing invitation from notification: " + invitationId);
         mInvitationFromNotification = null;
     }
 }
Пример #27
0
        public static void Create(Action <INearbyConnectionClient> callback)
        {
            if (Application.isEditor)
            {
                Logger.d("Creating INearbyConnection in editor, using DummyClient.");
                callback.Invoke(new DummyNearbyConnectionClient());
            }

            callback.Invoke(new AndroidNearbyConnectionClient());
        }
Пример #28
0
        /// <summary>
        /// Shows the standard Google Play Games achievements user interface,
        /// which allows the player to browse their achievements.
        /// </summary>
        public void ShowAchievementsUI()
        {
            if (!IsAuthenticated())
            {
                Logger.e("ShowAchievementsUI can only be called after authentication.");
                return;
            }

            Logger.d("ShowAchievementsUI");
            mClient.ShowAchievementsUI();
        }
        /// <summary>
        /// Signs in to Google Play Game Services.
        /// </summary>
        /// <param name="autoCloudLoad">
        /// Whether or not cloud data should be loaded automatically if the user is successfully signed in.
        /// Ignored if Cloud Saving is deactivated or the user fails to sign in.
        /// </param>
        /// <param name='callback'>
        /// The callback to call when authentication finishes. It will be called
        /// with <c>true</c> if authentication was successful, <c>false</c> otherwise.
        /// </param>
        public override void SignIn(bool autoCloudLoad = true, UnityAction <bool> callback = null)
        {
            if (!IsGpgsInitialized)
            {
                Debug.LogWarning("SignIn called, but Google Play Game Services has not been initialized. Ignoring call.");
                CloudOnceUtils.SafeInvoke(callback, false);
                return;
            }

            if (autoCloudLoad)
            {
                SetUpAutoCloudLoad();
            }

            IsGuestUserDefault = false;
            Logger.d("Attempting to sign in to Google Play Game Services.");

            PlayGamesPlatform.Instance.Authenticate(success =>
            {
                // Success is handled by OnAutenticated method
                if (!success)
                {
                    Logger.w("Failed to sign in to Google Play Game Services.");
                    bool hasNoInternet;
                    try
                    {
                        hasNoInternet = InternetConnectionUtils.GetConnectionStatus() != InternetConnectionStatus.Connected;
                    }
                    catch (NotSupportedException)
                    {
                        hasNoInternet = Application.internetReachability == NetworkReachability.NotReachable;
                    }

                    if (hasNoInternet)
                    {
                        Logger.d("Failure seems to be due to lack of Internet. Will try to connect again next time.");
                    }
                    else
                    {
                        Logger.d("Must assume the failure is due to player opting out"
                                 + " of the sign-in process, setting guest user as default");
                        IsGuestUserDefault = true;
                    }

                    cloudOnceEvents.RaiseOnSignInFailed();
                    if (autoCloudLoad)
                    {
                        cloudOnceEvents.RaiseOnCloudLoadComplete(false);
                    }
                }

                CloudOnceUtils.SafeInvoke(callback, success);
            });
        }
Пример #30
0
        // called on game thread
        public void TakeTurn(string matchId, byte[] data, string pendingParticipantId,
                             Action <bool> callback)
        {
            Logger.d(string.Format("AndroidTbmpClient.TakeTurn matchId={0}, data={1}, " +
                                   "pending={2}", matchId,
                                   (data == null ? "(null)" : "[" + data.Length + "bytes]"),
                                   pendingParticipantId));

            TbmpApiCall("tbmp take turn", "takeTurn", callback, null,
                        matchId, data, pendingParticipantId);
        }