HRESULT MultiplayerLobbyClient::CommitLobbyChanges()

in Source/Services/Multiplayer/Manager/multiplayer_lobby_client.cpp [739:1097]


HRESULT MultiplayerLobbyClient::CommitLobbyChanges(
    _In_ const Vector<uint64_t>& xuidsInOrder,
    _In_ std::shared_ptr<XblMultiplayerSession> lobbySessionToCommit,
    _In_ Callback<Result<void>> callback
) noexcept
{
    // For each User in xuidsInOrder, prepare an XblMultiplayerSession with their pending lobby changes
    // and write it to MPSD. Will also update the user's multiplayer activity & set the session host token
    // as neeeded. Consistent with 1806 XDK behavior, if any individual MPSD call fails unexpectedly,
    // we abort the entire operation and return that result.
    struct CommitLobbyChangesOperation : public std::enable_shared_from_this<CommitLobbyChangesOperation>
    {
        CommitLobbyChangesOperation(
            std::shared_ptr<MultiplayerLobbyClient> lobbyClient,
            const Vector<uint64_t>& xuidsInOrder,
            std::shared_ptr<XblMultiplayerSession> lobbySessionToCommit,
            Callback<Result<void>>&& callback
        ) noexcept :
            m_lobbyClient{ std::move(lobbyClient) },
            m_lobbySessionToCommit{ std::move(lobbySessionToCommit) },
            m_callback{ std::move(callback) }
        {
            auto& localUsers{ m_lobbyClient->GetLocalUserMap() };
            if (xuidsInOrder.empty())
            {
                for (auto pair : localUsers)
                {
                    m_users.push_back(pair.second);
                }
            }
            else
            {
                for (auto xuid : xuidsInOrder)
                {
                    auto iter = localUsers.find(xuid);
                    if (iter != localUsers.end())
                    {
                        m_users.push_back(iter->second);
                    }
                }
            }
        }

        void Run() noexcept
        {
            CommitChangesForNextUser();
        }

    private:
        void CommitChangesForNextUser() noexcept
        {
            if (m_users.empty())
            {
                Complete(S_OK);
            }
            else
            {
                auto user{ m_users.front() };
                m_users.pop_front();
                CommitChangesForUser(user);
            }
        }

        void CommitChangesForUser(
            std::shared_ptr<MultiplayerLocalUser> user
        ) noexcept
        {
            // Prepare a session with changes specific to this user.
            // Ok to use MPSD 'unsafe' methods here since we created the session locally, thus it is the only reference
            auto session = MakeShared<XblMultiplayerSession>(user->Xuid(), &m_lobbySessionToCommit->SessionReference(), nullptr);

            switch (user->LobbyState())
            {
            case MultiplayerLocalUserLobbyState::Add:
            {
                session->Join();
                if (!user->ConnectionAddress().empty())
                {
                    session->CurrentUserInternalUnsafe()->SetSecureDeviceBaseAddress64(user->ConnectionAddress());
                }
                session->SetSessionChangeSubscription(XblMultiplayerSessionChangeTypes::Everything);

                // Only set this for the first user. Could cause a race condition if XblMultiplayerJoinability was changed during the second write.
                if (m_setJoinability)
                {
                    m_setJoinability = false;
                    session->SetJoinRestriction(XblMultiplayerSessionRestriction::Followed);
                    session->SetReadRestriction(XblMultiplayerSessionRestriction::Followed);

                    String jsonValueStr = MultiplayerManagerUtils::ConvertJoinabilityToString(XblMultiplayerJoinability::JoinableByFriends);
                    JsonDocument jsonValue;
                    jsonValue.SetString(jsonValueStr.c_str(), jsonValue.GetAllocator());
                    session->SetSessionCustomPropertyJson(MultiplayerLobbyClient_JoinabilityPropertyName, jsonValue);
                }
                break;
            }
            case MultiplayerLocalUserLobbyState::Join:
            {
                session->Join();

                if (!user->ConnectionAddress().empty())
                {
                    session->CurrentUserInternalUnsafe()->SetSecureDeviceBaseAddress64(user->ConnectionAddress());
                }
                session->SetSessionChangeSubscription(XblMultiplayerSessionChangeTypes::Everything);
                break;
            }
            case MultiplayerLocalUserLobbyState::InSession:
            {
                // Forces to set the current user to yourself.
                session->Join();
                break;
            }
            case MultiplayerLocalUserLobbyState::Leave:
            {
                if (XblMultiplayerSession::IsPlayerInSession(user->Xuid(), m_lobbySessionToCommit))
                {
                    session->Leave();
                }
                else
                {
                    // In a scenario where the user was removed before he could have been added.
                    m_lobbyClient->UserStateChanged({ xbl_error_code::logic_error, "The user was removed before they could be added" }, MultiplayerLocalUserLobbyState::Add, user->Xuid());
                    m_lobbyClient->UserStateChanged({ xbl_error_code::no_error }, MultiplayerLocalUserLobbyState::Leave, user->Xuid());

                    m_removeStaleUsers = true;
                    user->SetLobbyState(MultiplayerLocalUserLobbyState::Remove);

                    // Since the user has been removed, no work to be done for this user
                    this->CommitChangesForNextUser();
                    return;
                }
                break;
            }
            default:
            {
                break;
            }
            }

            bool isGameInProgress = m_lobbyClient->GameSession() != nullptr;

            // Update any pending local user or lobby session properties.
            for (auto& request : m_lobbyClient->m_processingQueue)
            {
                request->AppendPendingChanges(session, user, isGameInProgress);
            }

            // Now write the session to MPSD
            HRESULT hr{ S_OK };
            if (user->LobbyHandleId().empty())
            {
                hr = m_lobbyClient->m_sessionWriter->WriteSession(
                    user->Context(),
                    session,
                    XblMultiplayerSessionWriteMode::UpdateOrCreateNew,
                    true,
                    [
                        op{ shared_from_this() },
                        user
                    ]
                (Result<std::shared_ptr<XblMultiplayerSession>> result)
                {
                    op->HandleWriteSessionResult(user, std::move(result));
                });
            }
            else
            {
                hr = m_lobbyClient->m_sessionWriter->WriteSessionByHandle(
                    user->Context(),
                    session,
                    XblMultiplayerSessionWriteMode::UpdateOrCreateNew,
                    user->LobbyHandleId(),
                    true,
                    [
                        op{ shared_from_this() },
                        user
                    ]
                (Result<std::shared_ptr<XblMultiplayerSession>> result)
                {
                    op->HandleWriteSessionResult(user, std::move(result));
                });

                user->SetLobbyHandleId(String{});
            }
            if (FAILED(hr))
            {
                Complete(hr);
            }
        }

        void HandleWriteSessionResult(
            std::shared_ptr<MultiplayerLocalUser> user,
            Result<std::shared_ptr<XblMultiplayerSession>>&& sessionResult
        ) noexcept
        {
            m_lobbyClient->UserStateChanged(sessionResult, user->LobbyState(), user->Xuid());
            m_lobbyClient->HandleLobbyChangeEvents(sessionResult, user, m_lobbyClient->m_processingQueue);

            if (Failed(sessionResult))
            {
                m_lobbyClient->JoinLobbyCompleted(sessionResult, user->Xuid());

                // If we failed to join the lobby, make sure the local user context is cleaned up
                switch (user->LobbyState())
                {
                case MultiplayerLocalUserLobbyState::Add:
                case MultiplayerLocalUserLobbyState::Join:
                {
                    m_removeStaleUsers = true;
                    user->SetLobbyState(MultiplayerLocalUserLobbyState::Remove);
                }
                default:
                {
                    break;
                }
                }

                Complete(sessionResult);
                return;
            }

            switch (user->LobbyState())
            {
            case MultiplayerLocalUserLobbyState::Add:
            case MultiplayerLocalUserLobbyState::Join:
            {
                auto updatedSession = sessionResult.ExtractPayload();
                m_lobbyClient->UpdateSession(updatedSession);
                auto oldLobbyState = user->LobbyState();
                user->SetLobbyState(MultiplayerLocalUserLobbyState::InSession);

                if (oldLobbyState == MultiplayerLocalUserLobbyState::Join)
                {
                    m_lobbyClient->HandleJoinLobbyCompleted(sessionResult, user->Xuid());
                }

                if (oldLobbyState == MultiplayerLocalUserLobbyState::Add && m_lobbyClient->ShouldUpdateHostToken(user, updatedSession))
                {
                    UpdateHostDeviceToken(user, updatedSession);
                }
                else
                {
                    SetActivityForUser(user, updatedSession);
                }
                return;
            }
            case MultiplayerLocalUserLobbyState::Leave:
            {
                m_removeStaleUsers = true;

                // If you leave the session you were advertising, you don't need to clear the activity.
                user->SetLobbyState(MultiplayerLocalUserLobbyState::Remove);

                // Intentional fallthrough
            }
            default:
            {
                // Work done for this user, continue operation
                CommitChangesForNextUser();
                return;
            }
            }
        }

        void UpdateHostDeviceToken(
            std::shared_ptr<MultiplayerLocalUser> user,
            std::shared_ptr<XblMultiplayerSession> session
        ) noexcept
        {
            XblMultiplayerSessionReadLockGuard sessionSafe{ session };
            session->SetHostDeviceToken(sessionSafe.CurrentUser()->DeviceToken);

            auto hr = m_lobbyClient->m_sessionWriter->WriteSession(
                user->Context(),
                session,
                XblMultiplayerSessionWriteMode::UpdateExisting,
                true,
                [
                    op{ shared_from_this() },
                    user
                ]
            (Result<std::shared_ptr<XblMultiplayerSession>> writeHostTokenResult)
            {
                if (Failed(writeHostTokenResult))
                {
                    op->Complete(writeHostTokenResult);
                }
                else
                {
                    op->SetActivityForUser(user, writeHostTokenResult.ExtractPayload());
                }
            });

            if (FAILED(hr))
            {
                Complete(hr);
            }
        }

        void SetActivityForUser(
            std::shared_ptr<MultiplayerLocalUser> user,
            std::shared_ptr<XblMultiplayerSession> session
        ) noexcept
        {
            auto hr = user->Context()->MultiplayerService()->SetActivity(
                session->SessionReference(),
                AsyncContext<Result<void>>{ m_lobbyClient->m_queue,
                [
                    op{ shared_from_this() }
                ]
            (Result<void> setActivityResult)
            {
                if (Failed(setActivityResult))
                {
                    op->Complete(std::move(setActivityResult));
                }
                else
                {
                    op->CommitChangesForNextUser();
                }
            }
            });

            if (FAILED(hr))
            {
                Complete(hr);
            }
        }

        void Complete(Result<void>&& result)
        {
            LOGS_DEBUG << __FUNCTION__ << ": HRESULT=" << result.Hresult() << ", ErrorMessage=" << result.ErrorMessage();

            if (m_removeStaleUsers)
            {
                m_lobbyClient->RemoveStaleXboxLiveContextFromMap();
            }
            m_callback(result);
        }

        std::shared_ptr<MultiplayerLobbyClient> m_lobbyClient;
        List<std::shared_ptr<MultiplayerLocalUser>> m_users;
        std::shared_ptr<XblMultiplayerSession> m_lobbySessionToCommit;
        bool m_setJoinability{ true };
        bool m_removeStaleUsers{ false };
        Callback<Result<void>> m_callback;
    };

    auto operation = MakeShared<CommitLobbyChangesOperation>(
        shared_from_this(),
        xuidsInOrder,
        lobbySessionToCommit,
        std::move(callback)
    );

    operation->Run();
    return S_OK;
}